use crate::capability::{
CapabilityDecision, CapabilityLedgerEntry, CapabilityScope, capabilities_from_tool_metadata,
};
use crate::effect::PostDecision;
use crate::event::AgentEvent;
use crate::runtime_components::{DefaultToolSecurityPolicy, ToolSecurityPolicy};
use crate::security::{SecurityDecision, SecurityPolicy};
use anyhow::Result;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value, json};
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use tokio::sync::mpsc;
pub mod background;
pub(crate) mod builtin;
pub mod metadata;
pub mod registry;
#[cfg(test)]
mod tests;
#[cfg(feature = "browser")]
use builtin::BrowserTool;
use builtin::{
AppendNoteTool, BashTool, CodeExecTool, ContextRemainingTool, CurrentTimeTool, EditTool,
HistoryOpsTool, InitSessionTool, MarkFeatureDoneTool, MemoryTool, MultiEditTool,
NewContextWindowTool, PackageManagerTool, PlanTool, QuestionTool, ReadTool,
RepoIntelligenceAction, RepoIntelligenceTool, RequestUserInputTool, RuntimeInfoTool,
SandboxTool, SearchTool, SleepTool, ToolSearchTool, ViewImageTool, WriteTool, builtin_metadata,
truncate_tool_result,
};
#[cfg(feature = "code-vfs")]
use builtin::{CodeEditTool, CodeReadTool};
pub use builtin::{
AgentBackend, AgentProfile, ApprovalMode, MockAgentBackend, PolicyAgentBackend,
ProviderBuilderFn, RepoExploreTool, SubagentBridgeBackend, SubagentTool, WorkerProbeBackend,
WorkflowTool, workflow_tool_description,
};
pub use metadata::{ToolExposure, ToolMetadata, ToolRisk, capabilities};
pub use registry::{ToolRegistry, ToolSet, phases};
#[async_trait]
pub trait Tool: Send + Sync {
fn definition(&self) -> ToolDefinition;
async fn invoke(&self, invocation: ToolInvocation) -> Result<ToolResult>;
async fn invoke_with_context(
&self,
invocation: ToolInvocation,
context: ToolInvocationContext,
) -> Result<ToolResult> {
let _ = context;
self.invoke(invocation).await
}
}
#[derive(Clone, Default)]
pub struct ToolInvocationContext {
pub event_tx: Option<mpsc::UnboundedSender<AgentEvent>>,
pub sudo_password_resolver: Option<crate::runtime::SudoPasswordResolver>,
pub cancel_token: Option<crate::cancel::CancelToken>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolDefinition {
pub name: String,
pub description: String,
pub kind: ToolKind,
#[serde(default)]
pub input_schema: Value,
#[serde(default)]
pub metadata: ToolMetadata,
}
impl Default for ToolDefinition {
fn default() -> Self {
Self {
name: String::new(),
description: String::new(),
kind: ToolKind::Custom,
input_schema: Value::Object(Default::default()),
metadata: ToolMetadata::default(),
}
}
}
impl ToolDefinition {
pub fn new(
name: impl Into<String>,
description: impl Into<String>,
kind: ToolKind,
input_schema: Value,
) -> Self {
Self {
name: name.into(),
description: description.into(),
kind,
input_schema,
metadata: ToolMetadata::default(),
}
}
pub fn with_metadata(
name: impl Into<String>,
description: impl Into<String>,
kind: ToolKind,
input_schema: Value,
metadata: ToolMetadata,
) -> Self {
Self {
name: name.into(),
description: description.into(),
kind,
input_schema,
metadata,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ToolKind {
Read,
Write,
Command,
Custom,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ToolParallelism {
Shared,
Exclusive,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolInvocation {
pub id: String,
pub tool_name: String,
pub input: Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolResult {
pub invocation_id: String,
pub ok: bool,
pub output: Value,
}
pub const NAVI_CONTENT_PARTS_KEY: &str = "_navi_content_parts";
pub fn take_tool_content_parts(result: &mut ToolResult) -> Vec<crate::model::ContentPart> {
let Some(obj) = result.output.as_object_mut() else {
return Vec::new();
};
let Some(raw) = obj.remove(NAVI_CONTENT_PARTS_KEY) else {
return Vec::new();
};
match serde_json::from_value::<Vec<crate::model::ContentPart>>(raw) {
Ok(parts) => parts,
Err(err) => {
tracing::warn!(error = %err, "failed to deserialize tool content_parts");
Vec::new()
}
}
}
pub struct ToolExecutor {
tools: HashMap<String, Arc<dyn Tool>>,
validators: HashMap<String, Arc<jsonschema::Validator>>,
invalid_schemas: HashMap<String, String>,
policy: SecurityPolicy,
security: Arc<dyn ToolSecurityPolicy>,
harness_profile: String,
registry: ToolRegistry,
rewind_store: Option<Arc<std::sync::Mutex<crate::rewind::RewindStore>>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ToolCallInvalid {
UnknownTool {
tool_name: String,
available_tools: Vec<String>,
},
InvalidSchema {
tool_name: String,
message: String,
},
MalformedArguments {
tool_name: String,
raw_arguments_preview: String,
example: Value,
},
InvalidArguments {
tool_name: String,
problems: Vec<String>,
example: Value,
},
}
const EXCLUSIVE_BATCH_TOOL_NAMES: &[&str] = &[
"plan", "question",
"subagent", "workflow",
];
impl ToolExecutor {
pub fn new(policy: SecurityPolicy) -> Self {
Self::with_security_policy(policy, Arc::new(DefaultToolSecurityPolicy))
}
pub fn empty(policy: SecurityPolicy) -> Self {
Self::empty_with_security_policy(policy, Arc::new(DefaultToolSecurityPolicy))
}
pub fn empty_with_security_policy(
policy: SecurityPolicy,
security: Arc<dyn ToolSecurityPolicy>,
) -> Self {
Self {
tools: HashMap::new(),
validators: HashMap::new(),
invalid_schemas: HashMap::new(),
policy,
security,
harness_profile: "medium".to_string(),
registry: ToolRegistry::new(),
rewind_store: None,
}
}
pub fn set_rewind_store(
&mut self,
store: Option<Arc<std::sync::Mutex<crate::rewind::RewindStore>>>,
) {
self.rewind_store = store;
}
pub fn rewind_store(&self) -> Option<Arc<std::sync::Mutex<crate::rewind::RewindStore>>> {
self.rewind_store.clone()
}
pub fn with_security_policy(
policy: SecurityPolicy,
security: Arc<dyn ToolSecurityPolicy>,
) -> Self {
let mut executor = Self::empty_with_security_policy(policy, security);
executor.register_builtin_tools();
executor
}
pub fn registry(&self) -> &ToolRegistry {
&self.registry
}
pub fn registry_mut(&mut self) -> &mut ToolRegistry {
&mut self.registry
}
pub fn search_tools(&self, query: &str, max_results: usize) -> Vec<ToolDefinition> {
self.registry.search(query, max_results)
}
pub fn set_harness_profile(&mut self, profile: String) {
self.harness_profile = profile;
self.register(RuntimeInfoTool::new(
self.policy.clone(),
self.harness_profile.clone(),
));
}
pub fn set_security_policy(&mut self, policy: SecurityPolicy) {
self.policy = policy;
self.register(RuntimeInfoTool::new(
self.policy.clone(),
self.harness_profile.clone(),
));
}
pub fn security_policy(&self) -> &SecurityPolicy {
&self.policy
}
pub fn register_skill_loader(
&mut self,
project_dir: std::path::PathBuf,
data_dir: std::path::PathBuf,
config: std::sync::Arc<std::sync::RwLock<crate::config::NaviConfig>>,
) {
let loader = crate::tool::builtin::SkillTool::new(
project_dir.clone(),
data_dir.clone(),
config.clone(),
);
self.register_tool(std::sync::Arc::new(loader));
self.register_tool(std::sync::Arc::new(
crate::tool::builtin::SkillListTool::new(
project_dir.clone(),
data_dir.clone(),
config.clone(),
),
));
self.register_tool(std::sync::Arc::new(
crate::tool::builtin::SkillGetTool::new(project_dir.clone(), data_dir.clone(), config),
));
self.register_tool(std::sync::Arc::new(
crate::tool::builtin::SkillSaveTool::new(project_dir.clone(), data_dir.clone()),
));
self.register_tool(std::sync::Arc::new(
crate::tool::builtin::SkillDeleteTool::new(project_dir, data_dir),
));
}
pub(crate) fn new_code_exec_host(policy: SecurityPolicy) -> Self {
let pr = policy.project_root().to_path_buf();
let mut executor = Self {
tools: HashMap::new(),
validators: HashMap::new(),
invalid_schemas: HashMap::new(),
policy: policy.clone(),
security: Arc::new(DefaultToolSecurityPolicy),
harness_profile: "medium".to_string(),
registry: ToolRegistry::new(),
rewind_store: None,
};
executor.register(ReadTool::new(pr.clone()));
executor.register(ReadTool::alias(pr.clone(), "read"));
executor.register(SearchTool::new(pr.clone()));
executor.register(SearchTool::grep(pr.clone()));
executor.register(SearchTool::fs_browser(pr.clone()));
executor.register(WriteTool::apply_patch(pr.clone()));
executor.register(BashTool::new(pr.clone()));
executor.register(RepoIntelligenceTool::new(
policy.clone(),
RepoIntelligenceAction::AstSearch,
));
executor.register(RepoIntelligenceTool::new(
policy,
RepoIntelligenceAction::TestDiscovery,
));
executor
}
pub fn definitions(&self) -> Vec<ToolDefinition> {
let visible_names: std::collections::HashSet<String> =
self.registry.visible_tool_names().into_iter().collect();
let mut result: Vec<ToolDefinition> = self
.tools
.values()
.filter(|tool| {
let def = tool.definition();
visible_names.contains(&def.name)
})
.map(|tool| {
let mut def = model_friendly_definition(tool.definition());
if let Some(registered) = self.registry.get(&def.name) {
def.metadata = registered.definition.metadata.clone();
}
def
})
.collect();
result.sort_by(|a, b| a.name.cmp(&b.name));
result
}
pub fn all_definitions(&self) -> Vec<ToolDefinition> {
let mut result = self
.tools
.values()
.map(|tool| model_friendly_definition(self.enriched_definition(tool.as_ref())))
.collect::<Vec<_>>();
result.sort_by(|a, b| a.name.cmp(&b.name));
result
}
pub fn definition(&self, name: &str) -> Option<ToolDefinition> {
self.tools
.get(name)
.map(|tool| self.enriched_definition(tool.as_ref()))
}
pub fn parallelism_for(&self, tool_name: &str) -> ToolParallelism {
if EXCLUSIVE_BATCH_TOOL_NAMES.contains(&tool_name) {
return ToolParallelism::Exclusive;
}
let Some(definition) = self.definition(tool_name) else {
return ToolParallelism::Shared;
};
if definition.metadata.is_read_only && definition.metadata.is_concurrency_safe {
ToolParallelism::Shared
} else {
ToolParallelism::Exclusive
}
}
pub fn register_tool(&mut self, tool: Arc<dyn Tool>) -> Option<Arc<dyn Tool>> {
let mut def = tool.definition();
let name = def.name.clone();
if def.metadata.is_default() {
let builtin = builtin_metadata(&name, def.kind);
def.metadata = builtin;
}
self.registry.register(def.clone());
match jsonschema::validator_for(&def.input_schema) {
Ok(v) => {
self.validators.insert(name.clone(), Arc::new(v));
self.invalid_schemas.remove(&name);
}
Err(e) => {
self.validators.remove(&name);
self.invalid_schemas.insert(name.clone(), e.to_string());
tracing::warn!(tool = %name, error = %e, "invalid schema");
}
}
self.tools.insert(name, tool)
}
pub fn validate_arguments(
&self,
inv: &ToolInvocation,
) -> std::result::Result<(), ToolCallInvalid> {
let Some(_) = self.definition(&inv.tool_name) else {
return Err(ToolCallInvalid::UnknownTool {
tool_name: inv.tool_name.clone(),
available_tools: self.tool_names(),
});
};
if let Some(e) = self.invalid_schemas.get(&inv.tool_name) {
return Err(ToolCallInvalid::InvalidSchema {
tool_name: inv.tool_name.clone(),
message: e.clone(),
});
}
if let Some(raw) = inv.input.get("raw_arguments").and_then(Value::as_str) {
return Err(ToolCallInvalid::MalformedArguments {
tool_name: inv.tool_name.clone(),
raw_arguments_preview: raw.chars().take(200).collect(),
example: self
.definition(&inv.tool_name)
.map(|d| example_from_schema(&d.input_schema))
.unwrap_or(json!({})),
});
}
let Some(v) = self.validators.get(&inv.tool_name) else {
return Err(ToolCallInvalid::InvalidSchema {
tool_name: inv.tool_name.clone(),
message: "missing validator".into(),
});
};
let errors: Vec<String> = v
.iter_errors(&inv.input)
.take(4)
.map(|e| {
let p = e.instance_path().to_string();
if p.is_empty() {
e.to_string()
} else {
format!("{e} at {p}")
}
})
.collect();
if !errors.is_empty() {
return Err(ToolCallInvalid::InvalidArguments {
tool_name: inv.tool_name.clone(),
problems: errors,
example: self
.definition(&inv.tool_name)
.map(|d| example_from_schema(&d.input_schema))
.unwrap_or(json!({})),
});
}
Ok(())
}
pub fn tool_names(&self) -> Vec<String> {
let mut n: Vec<String> = self.tools.keys().cloned().collect();
n.sort();
n
}
pub fn retain_tools<F>(&mut self, mut pred: F)
where
F: FnMut(&str) -> bool,
{
self.tools.retain(|n, _| pred(n));
self.validators.retain(|n, _| pred(n));
self.invalid_schemas.retain(|n, _| pred(n));
self.registry.retain_tools(|n| pred(n));
}
pub fn clear_tools(&mut self) {
self.tools.clear();
self.validators.clear();
self.invalid_schemas.clear();
self.registry.clear();
}
pub fn unregister_plugin_tools(&mut self) {
self.tools.retain(|n, _| !n.starts_with("plugin__"));
self.validators.retain(|n, _| !n.starts_with("plugin__"));
self.invalid_schemas
.retain(|n, _| !n.starts_with("plugin__"));
self.registry.unregister_prefix("plugin__");
}
pub fn invalid_tool_result(&self, inv: &ToolInvocation, err: ToolCallInvalid) -> ToolResult {
ToolResult {
invocation_id: inv.id.clone(),
ok: false,
output: tool_call_advice(err),
}
}
pub fn validate(&self, inv: &ToolInvocation) -> SecurityDecision {
if let Err(e) = self.validate_arguments(inv) {
return SecurityDecision::Deny(tool_call_advice_message(&e));
}
let Some(def) = self.definition(&inv.tool_name) else {
return SecurityDecision::Deny(format!("unknown `{}`", inv.tool_name));
};
self.security.validate_tool(&self.policy, &def, inv)
}
pub fn fork_with_policy_and_tools(
&self,
policy: SecurityPolicy,
allowed_tool_names: &[String],
) -> Self {
let mut forked = Self::empty_with_security_policy(policy, self.security.clone());
forked.harness_profile = self.harness_profile.clone();
forked.rewind_store = self.rewind_store.clone();
for name in allowed_tool_names {
if let Some(tool) = self.tools.get(name) {
forked.register_tool(tool.clone());
}
}
forked.retain_tools(|n| n != "subagent" && n != "workflow");
forked
}
pub fn policy(&self) -> &SecurityPolicy {
&self.policy
}
pub async fn invoke(&self, invocation: ToolInvocation) -> ToolResult {
self.invoke_with_event_tx(invocation, None).await
}
pub async fn invoke_with_event_tx(
&self,
invocation: ToolInvocation,
event_tx: Option<mpsc::UnboundedSender<AgentEvent>>,
) -> ToolResult {
self.invoke_with_context_inner(
invocation,
ToolInvocationContext {
event_tx,
..Default::default()
},
false,
)
.await
}
pub async fn invoke_approved_with_event_tx(
&self,
invocation: ToolInvocation,
event_tx: Option<mpsc::UnboundedSender<AgentEvent>>,
) -> ToolResult {
self.invoke_with_context_inner(
invocation,
ToolInvocationContext {
event_tx,
..Default::default()
},
true,
)
.await
}
pub async fn invoke_with_full_context(
&self,
invocation: ToolInvocation,
context: ToolInvocationContext,
approval_granted: bool,
) -> ToolResult {
self.invoke_with_context_inner(invocation, context, approval_granted)
.await
}
async fn invoke_with_context_inner(
&self,
invocation: ToolInvocation,
context: ToolInvocationContext,
approval_granted: bool,
) -> ToolResult {
let event_tx = context.event_tx.clone();
let inv_id = invocation.id.clone();
let started = std::time::Instant::now();
let invocation = self.policy.normalize_invocation_paths(&invocation);
let invocation =
recover_misnamed_tool_invocation(&invocation, |name| self.definition(name).is_some())
.unwrap_or(invocation);
let tool_name = invocation.tool_name.clone();
let tool_def = self.definition(&invocation.tool_name);
let tool_kind = tool_def.as_ref().map(|d| d.kind);
let tool_verifier_hint = tool_def
.as_ref()
.and_then(|d| d.metadata.verifier.as_deref())
.map(|v| v.to_string());
let capability_event_tx = event_tx.clone();
emit_capability_events(
capability_event_tx.as_ref(),
&invocation,
tool_def.as_ref(),
CapabilityDecision::Requested,
"tool invocation requested",
);
if let Err(e) = self.validate_arguments(&invocation) {
return self.invalid_tool_result(&invocation, e);
}
match self.validate(&invocation) {
SecurityDecision::Allow => {}
SecurityDecision::NeedsApproval(risk) if approval_granted => {
tracing::debug!(tool = %invocation.tool_name, ?risk, "tool approval already granted");
}
SecurityDecision::NeedsApproval(risk) => {
let message = format!(
"approval required for tool `{}`: {:?}",
invocation.tool_name, risk
);
emit_capability_events(
capability_event_tx.as_ref(),
&invocation,
tool_def.as_ref(),
CapabilityDecision::Denied,
&message,
);
return ToolResult {
invocation_id: inv_id,
ok: false,
output: json!({
"error_code": "approval_required",
"error": message,
"message": message,
"recoverable": true,
"hint": "Approve the tool request or switch permission mode (AcceptEdits/Auto/Yolo) if this should not require approval.",
}),
};
}
SecurityDecision::Deny(r) => {
emit_capability_events(
capability_event_tx.as_ref(),
&invocation,
tool_def.as_ref(),
CapabilityDecision::Denied,
&r,
);
return ToolResult {
invocation_id: inv_id,
ok: false,
output: json!({
"error_code": "security_denied",
"error": r,
"message": r,
"recoverable": true,
"hint": "Adjust the path/command or permission mode. Restricted mode keeps a project path jail; YOLO/AcceptEdits allow broader agency.",
}),
};
}
}
let Some(tool) = self.tools.get(&invocation.tool_name).cloned() else {
let message = format!("unknown `{}`", invocation.tool_name);
return ToolResult {
invocation_id: inv_id,
ok: false,
output: json!({
"error_code": "unknown_tool",
"error": message,
"message": message,
"recoverable": true,
"hint": "Use a registered tool name, or call tool_search to discover tools.",
}),
};
};
if invocation.tool_name == "tool_search" {
return self.invoke_tool_search(invocation);
}
let pre_execution_snapshot = if tool_kind == Some(crate::tool::ToolKind::Write) {
let paths = self.snapshot_paths_for_invocation(&invocation);
if let Some(store) = &self.rewind_store {
if !paths.is_empty() {
if let Ok(mut store) = store.lock() {
if let Err(e) = store.ensure_pre_write_capture(paths.iter().cloned()) {
tracing::debug!(error = %e, "rewind: pre-write capture failed");
}
}
}
}
if paths.is_empty() {
None
} else {
Some(crate::sandbox::SandboxManager::create_snapshot(&paths))
}
} else {
None
};
let inv_input = invocation.input.clone();
let mut result = match tool.invoke_with_context(invocation, context).await {
Ok(r) => truncate_tool_result(r),
Err(e) => ToolResult {
invocation_id: inv_id.clone(),
ok: false,
output: json!({"error": format!("{e:#}")}),
},
};
if result.ok && tool_kind == Some(crate::tool::ToolKind::Write) {
if let Some(store) = &self.rewind_store {
let paths = crate::effect::extract_paths(
&result,
&ToolInvocation {
id: inv_id.clone(),
tool_name: tool_name.clone(),
input: inv_input.clone(),
},
);
let abs_paths: Vec<std::path::PathBuf> = paths
.into_iter()
.map(|p| self.policy.resolve_project_path(&p))
.collect();
if !abs_paths.is_empty() {
if let Ok(mut store) = store.lock() {
store.note_written_paths(abs_paths);
}
}
}
}
if result.ok {
let should_check = match tool_kind {
Some(crate::tool::ToolKind::Write) => true,
Some(crate::tool::ToolKind::Command) => true,
_ => false,
};
if should_check {
let paths = crate::effect::extract_paths(
&result,
&ToolInvocation {
id: inv_id.clone(),
tool_name: tool_name.clone(),
input: inv_input,
},
);
if !paths.is_empty() {
let command = None;
let decision = self
.policy
.post_execution_effect_check(&tool_name, &paths, command);
match decision {
PostDecision::Allow => {
}
PostDecision::Ask(reason) => {
tracing::warn!(
tool = %tool_name,
reason = %reason,
"post-execution effect check: ask user"
);
if let Value::Object(ref mut map) = result.output {
map.insert(
"effect_warning".to_string(),
json!({
"decision": "ask",
"message": reason,
}),
);
}
}
PostDecision::Deny(reason) => {
tracing::warn!(
tool = %tool_name,
reason = %reason,
"post-execution effect check: denied"
);
let rollback = pre_execution_snapshot
.as_ref()
.map(crate::sandbox::SandboxManager::rollback);
let (rolled_back, rollback_error) = rollback_outcome(rollback);
emit_capability_events(
capability_event_tx.as_ref(),
&ToolInvocation {
id: inv_id.clone(),
tool_name: tool_name.clone(),
input: json!({}),
},
tool_def.as_ref(),
CapabilityDecision::Violated,
&reason,
);
return ToolResult {
invocation_id: inv_id,
ok: false,
output: json!({
"error": reason,
"error_code": "effect_denied",
"rolled_back": rolled_back,
"rollback_error": rollback_error,
}),
};
}
PostDecision::Rollback(reason) => {
tracing::warn!(
tool = %tool_name,
reason = %reason,
"post-execution effect check: rollback recommended"
);
let rollback = pre_execution_snapshot
.as_ref()
.map(crate::sandbox::SandboxManager::rollback);
let (rolled_back, rollback_error) = rollback_outcome(rollback);
emit_capability_events(
capability_event_tx.as_ref(),
&ToolInvocation {
id: inv_id.clone(),
tool_name: tool_name.clone(),
input: json!({}),
},
tool_def.as_ref(),
CapabilityDecision::Violated,
&reason,
);
return ToolResult {
invocation_id: inv_id,
ok: false,
output: json!({
"error": reason,
"error_code": "effect_rollback",
"rolled_back": rolled_back,
"rollback_error": rollback_error,
}),
};
}
}
}
}
}
emit_capability_events(
capability_event_tx.as_ref(),
&ToolInvocation {
id: inv_id.clone(),
tool_name: tool_name.clone(),
input: json!({}),
},
tool_def.as_ref(),
if result.ok {
CapabilityDecision::Consumed
} else {
CapabilityDecision::Violated
},
if result.ok {
"tool invocation completed"
} else {
"tool invocation failed"
},
);
if result.ok && tool_kind == Some(crate::tool::ToolKind::Write) {
if let Some(verifier_cmd) = tool_verifier_hint {
if let Value::Object(ref mut map) = result.output {
map.insert(
"verifier_hint".to_string(),
json!({
"suggested": true,
"command": verifier_cmd,
"message": format!(
"After writing, verify with: verifier(action='run', verifier='command', command='{}')",
verifier_cmd
),
}),
);
}
}
}
tracing::info!(tool = %tool_name, ok = result.ok, dur_ms = started.elapsed().as_millis() as u64, "invoke finished");
result
}
fn invoke_tool_search(&self, invocation: ToolInvocation) -> ToolResult {
let query = invocation
.input
.get("query")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();
let max_results = invocation
.input
.get("max_results")
.and_then(Value::as_u64)
.unwrap_or(10)
.min(50) as usize;
let results = self.registry.search(&query, max_results);
let results = results
.into_iter()
.map(model_friendly_definition)
.map(|def| {
json!({
"name": def.name,
"description": def.description,
"kind": def.kind,
"metadata": def.metadata,
"input_schema": def.input_schema,
})
})
.collect::<Vec<_>>();
ToolResult {
invocation_id: invocation.id,
ok: true,
output: json!({
"query": query,
"results": results,
"total": results.len(),
"hint": if results.is_empty() {
"No tools found. Try broader terms like: code, browser, package, memory, subagent, sandbox, goal."
} else {
"These tools may be deferred (not always in the schema). Call a returned tool by its `name` with arguments matching `input_schema`."
},
"power_catalog": [
"code / code_edit / ast_search / symbol_*: symbols and structured code nav",
"repo_explore: BM25 semantic search",
"package_manager: dependency install/add/update",
"browser: headless UI testing",
"subagent: nested agent",
"apply_patch / sandbox / set_goal / history_ops: advanced workflows",
],
}),
}
}
fn snapshot_paths_for_invocation(
&self,
invocation: &ToolInvocation,
) -> Vec<std::path::PathBuf> {
let mut paths = Vec::new();
for key in ["path", "file", "file_path"] {
if let Some(path) = invocation.input.get(key).and_then(Value::as_str) {
push_unique_snapshot_path(
&mut paths,
self.policy.resolve_project_path(Path::new(path)),
);
}
}
if let Some(patch) = invocation.input.get("patch").and_then(Value::as_str) {
for path in crate::security::extract_apply_patch_paths(patch) {
push_unique_snapshot_path(
&mut paths,
self.policy.resolve_project_path(Path::new(&path)),
);
}
}
if let Some(patches) = invocation.input.get("patches").and_then(Value::as_array) {
for patch in patches.iter().filter_map(Value::as_str) {
for path in crate::security::extract_apply_patch_paths(patch) {
push_unique_snapshot_path(
&mut paths,
self.policy.resolve_project_path(Path::new(&path)),
);
}
}
}
paths
}
pub async fn list_background_commands(&self) -> Vec<background::BackgroundCommandSnapshot> {
let r = self
.invoke(ToolInvocation {
id: "bg-list".into(),
tool_name: "bash".into(),
input: json!({"action": "list"}),
})
.await;
r.output
.get("tasks")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(background::BackgroundCommandSnapshot::from_json)
.collect()
})
.unwrap_or_default()
}
pub async fn poll_background_command(
&self,
task_id: &str,
) -> Option<background::BackgroundCommandSnapshot> {
let r = self
.invoke(ToolInvocation {
id: "bg-poll".into(),
tool_name: "bash".into(),
input: json!({"task_id": task_id}),
})
.await;
if r.ok {
background::BackgroundCommandSnapshot::from_json(&r.output)
} else {
None
}
}
pub async fn cancel_background_command(
&self,
task_id: &str,
) -> Option<background::BackgroundCommandSnapshot> {
let r = self
.invoke(ToolInvocation {
id: "bg-cancel".into(),
tool_name: "bash".into(),
input: json!({"task_id": task_id, "action": "cancel"}),
})
.await;
if r.ok {
background::BackgroundCommandSnapshot::from_json(&r.output)
} else {
None
}
}
fn register(&mut self, tool: impl Tool + 'static) {
self.register_tool(Arc::new(tool));
}
fn enriched_definition(&self, tool: &dyn Tool) -> ToolDefinition {
let mut def = tool.definition();
if let Some(registered) = self.registry.get(&def.name) {
def.metadata = registered.definition.metadata.clone();
}
def
}
fn register_builtin_tools(&mut self) {
let pr = self.policy.project_root().to_path_buf();
self.register(ReadTool::new(pr.clone())); self.register(ReadTool::alias(pr.clone(), "read")); self.register(SearchTool::new(pr.clone()));
self.register(SearchTool::grep(pr.clone()));
self.register(SearchTool::fs_browser(pr.clone()));
self.register(SearchTool::list_dir(pr.clone()));
self.register(SearchTool::glob(pr.clone()));
self.register(EditTool::new(pr.clone()));
self.register(MultiEditTool::new(pr.clone()));
self.register(WriteTool::new(pr.clone()));
self.register(WriteTool::write_file(pr.clone()));
self.register(WriteTool::apply_patch(pr.clone()));
self.register(BashTool::new(pr.clone()));
self.register(QuestionTool);
self.register(PlanTool::new(self.policy.clone()));
self.register(PackageManagerTool::new(pr.clone()));
self.register(RuntimeInfoTool::new(
self.policy.clone(),
self.harness_profile.clone(),
));
#[cfg(feature = "code-vfs")]
{
self.register(CodeReadTool::new(self.policy.clone()));
self.register(CodeEditTool::new(self.policy.clone()));
}
self.register(CodeExecTool::new(self.policy.clone()));
self.register(RepoIntelligenceTool::new(
self.policy.clone(),
RepoIntelligenceAction::AstSearch,
));
self.register(RepoIntelligenceTool::new(
self.policy.clone(),
RepoIntelligenceAction::SymbolGoto,
));
self.register(RepoIntelligenceTool::new(
self.policy.clone(),
RepoIntelligenceAction::SymbolReferences,
));
self.register(RepoIntelligenceTool::new(
self.policy.clone(),
RepoIntelligenceAction::DependencyGraph,
));
self.register(RepoIntelligenceTool::new(
self.policy.clone(),
RepoIntelligenceAction::TestDiscovery,
));
self.register(RepoIntelligenceTool::new(
self.policy.clone(),
RepoIntelligenceAction::OwnershipChurn,
));
self.register(InitSessionTool::new(self.policy.clone()));
self.register(MarkFeatureDoneTool::new(self.policy.clone()));
self.register(AppendNoteTool::new(pr.clone()));
self.register(MemoryTool::new(pr.clone()));
self.register(HistoryOpsTool::new(pr.clone()));
self.register(CurrentTimeTool::new());
self.register(SleepTool::new());
self.register(ContextRemainingTool::new(pr.clone()));
self.register(RequestUserInputTool::new());
self.register(SandboxTool::new(pr.clone()));
let data_dir = self.policy.data_dir().to_path_buf();
self.register(ViewImageTool::new(pr.clone(), data_dir.clone()));
self.register(ViewImageTool::inspect_image(pr.clone(), data_dir));
#[cfg(feature = "browser")]
self.register(BrowserTool::new(pr.clone()));
self.register(NewContextWindowTool::new());
self.register(ToolSearchTool::new(Arc::new(self.registry.clone())));
self.register(WorkflowTool::new(
self.policy.clone(),
crate::config::WorkflowConfig::default(),
));
}
}
fn tool_call_advice(err: ToolCallInvalid) -> Value {
match err {
ToolCallInvalid::UnknownTool {
tool_name,
available_tools,
} => {
let message = "Requested tool is not registered. Use one of the available tool names."
.to_string();
json!({
"error_code": "unknown_tool",
"error_kind": "unknown_tool",
"tool": tool_name,
"error": message,
"message": message,
"hint": "Call tool_search or use a name from available_tools.",
"recoverable": true,
"suggestions": suggest_tool_replacements(&tool_name, &available_tools),
"available_tools": available_tools.into_iter().take(20).collect::<Vec<_>>(),
})
}
ToolCallInvalid::InvalidSchema { tool_name, message } => {
let message = format!("Tool schema is invalid: {message}");
json!({
"error_code": "invalid_schema",
"error_kind": "invalid_schema",
"tool": tool_name,
"error": message,
"message": message,
"recoverable": false,
})
}
ToolCallInvalid::MalformedArguments {
tool_name,
raw_arguments_preview,
example,
} => {
let message = "Tool arguments were not valid JSON. Emit one complete JSON object matching the schema before calling the tool again.".to_string();
json!({
"error_code": "invalid_arguments",
"error_kind": "malformed_arguments",
"tool": tool_name,
"error": message,
"message": message,
"hint": "Emit a single complete JSON object; do not wrap arguments in markdown fences.",
"recoverable": true,
"raw_arguments_preview": raw_arguments_preview,
"example": example,
})
}
ToolCallInvalid::InvalidArguments {
tool_name,
problems,
example,
} => {
let message = "Tool arguments do not match the JSON schema. Fix the arguments and call the tool again.".to_string();
json!({
"error_code": "invalid_arguments",
"error_kind": "invalid_arguments",
"tool": tool_name,
"error": message,
"message": message,
"hint": "Compare your arguments to the tool schema and the example.",
"recoverable": true,
"problems": problems,
"example": example,
})
}
}
}
fn tool_call_advice_message(err: &ToolCallInvalid) -> String {
match err {
ToolCallInvalid::UnknownTool { tool_name, .. } => format!("unknown tool `{tool_name}`"),
ToolCallInvalid::InvalidSchema { tool_name, message } => {
format!("invalid schema for `{tool_name}`: {message}")
}
ToolCallInvalid::MalformedArguments { tool_name, .. } => {
format!("malformed args for `{tool_name}`")
}
ToolCallInvalid::InvalidArguments {
tool_name,
problems,
..
} => format!("invalid args for `{tool_name}`: {}", problems.join("; ")),
}
}
fn model_friendly_definition(mut definition: ToolDefinition) -> ToolDefinition {
definition.input_schema = simplify_schema_for_model(&definition.input_schema);
definition
}
fn simplify_schema_for_model(schema: &Value) -> Value {
match schema {
Value::Object(object) => simplify_schema_object_for_model(object),
Value::Array(values) => {
Value::Array(values.iter().map(simplify_schema_for_model).collect())
}
value => value.clone(),
}
}
fn simplify_schema_object_for_model(object: &Map<String, Value>) -> Value {
let mut simplified = Map::new();
for keyword in ["oneOf", "anyOf", "allOf"] {
let Some(branches) = object.get(keyword).and_then(Value::as_array) else {
continue;
};
if let Some(Value::Object(branch)) = branches.first().map(simplify_schema_for_model) {
simplified.extend(branch);
}
break;
}
for (key, value) in object {
if matches!(key.as_str(), "oneOf" | "anyOf" | "allOf" | "const") {
continue;
}
simplified.insert(key.clone(), simplify_schema_for_model(value));
}
Value::Object(simplified)
}
fn suggest_tool_replacements(tool_name: &str, available_tools: &[String]) -> Vec<String> {
let lower = tool_name.trim().to_ascii_lowercase();
let candidates: &[&str] = match lower.as_str() {
"list_files" | "ls" | "listdir" | "dir" | "list" | "list_dir" | "find" | "find_files"
| "grep" | "glob" | "fs_browser" => &["search"],
"cat" | "type" | "open" | "view_file" | "read" | "view" => &["read_file"],
"patch" | "str_replace" | "search_replace" | "searchreplace" | "multiedit"
| "multi_edit" | "multi-edit" | "batched_edit" | "apply_patch" => &["edit", "write_file"],
"request_user_input" | "ask_user" | "ask" => &["question"],
"shell" | "run" | "terminal" | "exec" | "sh" | "cmd" | "process" => &["bash"],
"rg" | "ripgrep" | "search_code" => &["search", "code"],
"symbols" | "symbol" | "symbols_overview" | "find_symbol" | "find_references"
| "code_diagnostics" => &["code", "ast_search", "symbol_goto"],
"replace_symbol_body"
| "insert_before_symbol"
| "insert_after_symbol"
| "rename_symbol" => &["code_edit"],
_ if looks_like_filesystem_path(tool_name) => &["read_file", "fs_browser", "grep", "bash"],
_ => &[],
};
candidates
.iter()
.filter(|candidate| available_tools.iter().any(|tool| tool == **candidate))
.map(|candidate| (*candidate).to_string())
.collect()
}
fn recover_misnamed_tool_invocation(
inv: &ToolInvocation,
has_tool: impl Fn(&str) -> bool,
) -> Option<ToolInvocation> {
if has_tool(&inv.tool_name) {
return None;
}
let name = inv.tool_name.trim();
if name.is_empty() {
return None;
}
let input = inv.input.as_object();
let action = input
.and_then(|obj| obj.get("action"))
.and_then(Value::as_str)
.map(|s| s.to_ascii_lowercase());
let has_path_field = input
.and_then(|obj| obj.get("path"))
.and_then(Value::as_str)
.is_some_and(|p| !p.is_empty());
let path_from_input = input
.and_then(|obj| obj.get("path"))
.and_then(Value::as_str)
.map(str::to_string);
let path_like_name = looks_like_filesystem_path(name);
if matches!(
action.as_deref(),
Some("list" | "tree" | "find" | "stat" | "read" | "search")
) && has_tool("fs_browser")
{
let mut recovered = inv.clone();
recovered.tool_name = "fs_browser".to_string();
if !has_path_field && path_like_name {
if let Some(obj) = recovered.input.as_object_mut() {
obj.insert("path".to_string(), Value::String(name.to_string()));
}
}
tracing::info!(
from = %inv.tool_name,
to = "fs_browser",
"recovered misnamed tool invocation"
);
return Some(recovered);
}
if path_like_name && has_tool("read_file") {
let keys: Vec<&str> = input
.map(|obj| obj.keys().map(String::as_str).collect())
.unwrap_or_default();
let readish = keys.is_empty()
|| keys.iter().all(|k| {
matches!(
*k,
"path"
| "offset"
| "limit"
| "start_line"
| "end_line"
| "max_bytes"
| "encoding"
)
});
let writeish = keys
.iter()
.any(|k| matches!(*k, "content" | "patch" | "patches" | "edits" | "new_string"));
if readish && !writeish {
let mut recovered = inv.clone();
recovered.tool_name = "read_file".to_string();
if !has_path_field {
let mut obj = serde_json::Map::new();
obj.insert("path".to_string(), Value::String(name.to_string()));
if let Some(input_obj) = input {
for (k, v) in input_obj {
if k != "path" {
obj.insert(k.clone(), v.clone());
}
}
}
recovered.input = Value::Object(obj);
} else if path_from_input.as_deref() != Some(name)
&& path_from_input
.as_deref()
.is_some_and(|p| p == "." || p.is_empty())
{
if let Some(obj) = recovered.input.as_object_mut() {
obj.insert("path".to_string(), Value::String(name.to_string()));
}
}
tracing::info!(
from = %inv.tool_name,
to = "read_file",
"recovered misnamed tool invocation"
);
return Some(recovered);
}
}
None
}
fn looks_like_filesystem_path(name: &str) -> bool {
let name = name.trim();
if name.is_empty() {
return false;
}
if name == "." || name == ".." {
return true;
}
if name.contains('/') || name.contains('\\') {
return true;
}
if let Some((_, ext)) = name.rsplit_once('.') {
let ext = ext.trim();
if !ext.is_empty()
&& ext.len() <= 12
&& ext
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '-')
&& !name.starts_with('.')
&& (ext.len() <= 8)
{
return true;
}
}
false
}
fn push_unique_snapshot_path(paths: &mut Vec<std::path::PathBuf>, path: std::path::PathBuf) {
if !paths.contains(&path) {
paths.push(path);
}
}
fn rollback_outcome(rollback: Option<std::result::Result<(), String>>) -> (bool, Option<String>) {
match rollback {
Some(Ok(())) => (true, None),
Some(Err(error)) => (false, Some(error)),
None => (false, None),
}
}
fn emit_capability_events(
event_tx: Option<&mpsc::UnboundedSender<AgentEvent>>,
invocation: &ToolInvocation,
definition: Option<&ToolDefinition>,
decision: CapabilityDecision,
justification: &str,
) {
let Some(event_tx) = event_tx else {
return;
};
let Some(definition) = definition else {
return;
};
for capability in capabilities_from_tool_metadata(&definition.metadata.capabilities) {
let _ = event_tx.send(AgentEvent::CapabilityRecorded(CapabilityLedgerEntry {
capability,
scope: CapabilityScope::SingleCall(invocation.id.clone()),
decision: decision.clone(),
at_ms: tool_unix_millis(),
justification: format!("{}: {justification}", invocation.tool_name),
}));
}
}
fn tool_unix_millis() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|duration| duration.as_millis() as u64)
.unwrap_or(0)
}
pub fn example_from_schema(schema: &Value) -> Value {
if let Some(ex) = schema
.get("examples")
.and_then(Value::as_array)
.and_then(|a| a.first())
{
return ex.clone();
}
let Some(properties) = schema.get("properties").and_then(Value::as_object) else {
return json!({});
};
let required: Vec<&str> = schema
.get("required")
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter_map(Value::as_str)
.collect();
let mut ex = serde_json::Map::new();
for field in required {
let v = properties
.get(field)
.and_then(|p| p.get("type"))
.and_then(Value::as_str)
.map(|k| match k {
"integer" => json!(1),
"number" => json!(1.0),
"boolean" => json!(true),
"array" => json!([]),
"object" => json!({}),
_ => json!("example"),
})
.unwrap_or(json!("example"));
ex.insert(field.to_string(), v);
}
Value::Object(ex)
}