1use crate::capability::{
2 CapabilityDecision, CapabilityLedgerEntry, CapabilityScope, capabilities_from_tool_metadata,
3};
4use crate::effect::PostDecision;
5use crate::event::AgentEvent;
6use crate::runtime_components::{DefaultToolSecurityPolicy, ToolSecurityPolicy};
7use crate::security::{SecurityDecision, SecurityPolicy};
8use anyhow::Result;
9use async_trait::async_trait;
10use serde::{Deserialize, Serialize};
11use serde_json::{Map, Value, json};
12use std::collections::HashMap;
13use std::path::Path;
14use std::sync::Arc;
15use tokio::sync::mpsc;
16
17pub mod background;
18pub(crate) mod builtin;
19pub mod metadata;
20pub mod registry;
21#[cfg(test)]
22mod tests;
23
24#[cfg(feature = "browser")]
25use builtin::BrowserTool;
26use builtin::{
27 AppendNoteTool, BashTool, CodeExecTool, ContextRemainingTool, CurrentTimeTool, EditTool,
28 HistoryOpsTool, InitSessionTool, MarkFeatureDoneTool, MemoryTool, MultiEditTool,
29 NewContextWindowTool, PackageManagerTool, PlanTool, QuestionTool, ReadTool,
30 RepoIntelligenceAction, RepoIntelligenceTool, RequestUserInputTool, RuntimeInfoTool,
31 SandboxTool, SearchTool, SetGoalTool, SleepTool, ToolSearchTool, ViewImageTool, WriteTool,
32 builtin_metadata, truncate_tool_result,
33};
34#[cfg(feature = "code-vfs")]
35use builtin::{CodeEditTool, CodeReadTool};
36
37pub use builtin::{AgentProfile, ApprovalMode, ProviderBuilderFn, RepoExploreTool, SubagentTool};
38pub use metadata::{ToolExposure, ToolMetadata, ToolRisk, capabilities};
39pub use registry::{ToolRegistry, ToolSet, phases};
40
41#[async_trait]
42pub trait Tool: Send + Sync {
43 fn definition(&self) -> ToolDefinition;
44 async fn invoke(&self, invocation: ToolInvocation) -> Result<ToolResult>;
45 async fn invoke_with_context(
46 &self,
47 invocation: ToolInvocation,
48 context: ToolInvocationContext,
49 ) -> Result<ToolResult> {
50 let _ = context;
51 self.invoke(invocation).await
52 }
53}
54
55#[derive(Clone, Default)]
56pub struct ToolInvocationContext {
57 pub event_tx: Option<mpsc::UnboundedSender<AgentEvent>>,
58 pub sudo_password_resolver: Option<crate::runtime::SudoPasswordResolver>,
60 pub cancel_token: Option<crate::cancel::CancelToken>,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct ToolDefinition {
65 pub name: String,
66 pub description: String,
67 pub kind: ToolKind,
68 #[serde(default)]
69 pub input_schema: Value,
70 #[serde(default)]
73 pub metadata: ToolMetadata,
74}
75
76impl Default for ToolDefinition {
77 fn default() -> Self {
78 Self {
79 name: String::new(),
80 description: String::new(),
81 kind: ToolKind::Custom,
82 input_schema: Value::Object(Default::default()),
83 metadata: ToolMetadata::default(),
84 }
85 }
86}
87
88impl ToolDefinition {
89 pub fn new(
91 name: impl Into<String>,
92 description: impl Into<String>,
93 kind: ToolKind,
94 input_schema: Value,
95 ) -> Self {
96 Self {
97 name: name.into(),
98 description: description.into(),
99 kind,
100 input_schema,
101 metadata: ToolMetadata::default(),
102 }
103 }
104
105 pub fn with_metadata(
107 name: impl Into<String>,
108 description: impl Into<String>,
109 kind: ToolKind,
110 input_schema: Value,
111 metadata: ToolMetadata,
112 ) -> Self {
113 Self {
114 name: name.into(),
115 description: description.into(),
116 kind,
117 input_schema,
118 metadata,
119 }
120 }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
124pub enum ToolKind {
125 Read,
126 Write,
127 Command,
128 Custom,
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
132pub enum ToolParallelism {
133 Shared,
135 Exclusive,
137}
138
139#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
140pub struct ToolInvocation {
141 pub id: String,
142 pub tool_name: String,
143 pub input: Value,
144}
145
146#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
147pub struct ToolResult {
148 pub invocation_id: String,
149 pub ok: bool,
150 pub output: Value,
151}
152
153pub const NAVI_CONTENT_PARTS_KEY: &str = "_navi_content_parts";
156
157pub fn take_tool_content_parts(
163 result: &mut ToolResult,
164) -> Vec<crate::model::ContentPart> {
165 let Some(obj) = result.output.as_object_mut() else {
166 return Vec::new();
167 };
168 let Some(raw) = obj.remove(NAVI_CONTENT_PARTS_KEY) else {
169 return Vec::new();
170 };
171 match serde_json::from_value::<Vec<crate::model::ContentPart>>(raw) {
172 Ok(parts) => parts,
173 Err(err) => {
174 tracing::warn!(error = %err, "failed to deserialize tool content_parts");
175 Vec::new()
176 }
177 }
178}
179
180pub struct ToolExecutor {
181 tools: HashMap<String, Arc<dyn Tool>>,
182 validators: HashMap<String, Arc<jsonschema::Validator>>,
183 invalid_schemas: HashMap<String, String>,
184 policy: SecurityPolicy,
185 security: Arc<dyn ToolSecurityPolicy>,
186 harness_profile: String,
187 registry: ToolRegistry,
188}
189
190#[derive(Debug, Clone, PartialEq, Eq)]
191pub enum ToolCallInvalid {
192 UnknownTool {
193 tool_name: String,
194 available_tools: Vec<String>,
195 },
196 InvalidSchema {
197 tool_name: String,
198 message: String,
199 },
200 MalformedArguments {
201 tool_name: String,
202 raw_arguments_preview: String,
203 example: Value,
204 },
205 InvalidArguments {
206 tool_name: String,
207 problems: Vec<String>,
208 example: Value,
209 },
210}
211
212const EXCLUSIVE_BATCH_TOOL_NAMES: &[&str] = &[
213 "plan", "question",
214 "subagent",
217 ];
219
220impl ToolExecutor {
221 pub fn new(policy: SecurityPolicy) -> Self {
222 Self::with_security_policy(policy, Arc::new(DefaultToolSecurityPolicy))
223 }
224
225 pub fn empty(policy: SecurityPolicy) -> Self {
226 Self::empty_with_security_policy(policy, Arc::new(DefaultToolSecurityPolicy))
227 }
228
229 pub fn empty_with_security_policy(
230 policy: SecurityPolicy,
231 security: Arc<dyn ToolSecurityPolicy>,
232 ) -> Self {
233 Self {
234 tools: HashMap::new(),
235 validators: HashMap::new(),
236 invalid_schemas: HashMap::new(),
237 policy,
238 security,
239 harness_profile: "medium".to_string(),
240 registry: ToolRegistry::new(),
241 }
242 }
243
244 pub fn with_security_policy(
245 policy: SecurityPolicy,
246 security: Arc<dyn ToolSecurityPolicy>,
247 ) -> Self {
248 let mut executor = Self::empty_with_security_policy(policy, security);
249 executor.register_builtin_tools();
250 executor
251 }
252
253 pub fn registry(&self) -> &ToolRegistry {
254 &self.registry
255 }
256
257 pub fn registry_mut(&mut self) -> &mut ToolRegistry {
258 &mut self.registry
259 }
260
261 pub fn search_tools(&self, query: &str, max_results: usize) -> Vec<ToolDefinition> {
263 self.registry.search(query, max_results)
264 }
265
266 pub fn set_harness_profile(&mut self, profile: String) {
267 self.harness_profile = profile;
268 self.register(RuntimeInfoTool::new(
269 self.policy.clone(),
270 self.harness_profile.clone(),
271 ));
272 }
273
274 pub fn set_security_policy(&mut self, policy: SecurityPolicy) {
276 self.policy = policy;
277 self.register(RuntimeInfoTool::new(
278 self.policy.clone(),
279 self.harness_profile.clone(),
280 ));
281 }
282
283 pub fn security_policy(&self) -> &SecurityPolicy {
285 &self.policy
286 }
287
288 pub fn register_skill_loader(
289 &mut self,
290 project_dir: std::path::PathBuf,
291 data_dir: std::path::PathBuf,
292 config: std::sync::Arc<std::sync::RwLock<crate::config::NaviConfig>>,
293 ) {
294 let loader = crate::tool::builtin::SkillTool::new(
296 project_dir.clone(),
297 data_dir.clone(),
298 config.clone(),
299 );
300 self.register_tool(std::sync::Arc::new(loader));
301 self.register_tool(std::sync::Arc::new(
302 crate::tool::builtin::SkillListTool::new(
303 project_dir.clone(),
304 data_dir.clone(),
305 config.clone(),
306 ),
307 ));
308 self.register_tool(std::sync::Arc::new(
309 crate::tool::builtin::SkillGetTool::new(project_dir.clone(), data_dir.clone(), config),
310 ));
311 self.register_tool(std::sync::Arc::new(
312 crate::tool::builtin::SkillSaveTool::new(project_dir.clone(), data_dir.clone()),
313 ));
314 self.register_tool(std::sync::Arc::new(
315 crate::tool::builtin::SkillDeleteTool::new(project_dir, data_dir),
316 ));
317 }
318
319 pub(crate) fn new_code_exec_host(policy: SecurityPolicy) -> Self {
320 let pr = policy.project_root().to_path_buf();
321 let mut executor = Self {
322 tools: HashMap::new(),
323 validators: HashMap::new(),
324 invalid_schemas: HashMap::new(),
325 policy: policy.clone(),
326 security: Arc::new(DefaultToolSecurityPolicy),
327 harness_profile: "medium".to_string(),
328 registry: ToolRegistry::new(),
329 };
330 executor.register(ReadTool::new(pr.clone()));
331 executor.register(ReadTool::alias(pr.clone(), "read"));
332 executor.register(SearchTool::new(pr.clone()));
333 executor.register(SearchTool::grep(pr.clone()));
334 executor.register(SearchTool::fs_browser(pr.clone()));
335 executor.register(WriteTool::apply_patch(pr.clone()));
336 executor.register(BashTool::new(pr.clone()));
337 executor.register(RepoIntelligenceTool::new(
338 policy.clone(),
339 RepoIntelligenceAction::AstSearch,
340 ));
341 executor.register(RepoIntelligenceTool::new(
342 policy,
343 RepoIntelligenceAction::TestDiscovery,
344 ));
345 executor
346 }
347
348 pub fn definitions(&self) -> Vec<ToolDefinition> {
349 let visible_names: std::collections::HashSet<String> =
354 self.registry.visible_tool_names().into_iter().collect();
355
356 let mut result: Vec<ToolDefinition> = self
357 .tools
358 .values()
359 .filter(|tool| {
360 let def = tool.definition();
361 visible_names.contains(&def.name)
362 })
363 .map(|tool| {
364 let mut def = model_friendly_definition(tool.definition());
365 if let Some(registered) = self.registry.get(&def.name) {
367 def.metadata = registered.definition.metadata.clone();
368 }
369 def
370 })
371 .collect();
372 result.sort_by(|a, b| a.name.cmp(&b.name));
373 result
374 }
375
376 pub fn all_definitions(&self) -> Vec<ToolDefinition> {
377 let mut result = self
378 .tools
379 .values()
380 .map(|tool| model_friendly_definition(self.enriched_definition(tool.as_ref())))
381 .collect::<Vec<_>>();
382 result.sort_by(|a, b| a.name.cmp(&b.name));
383 result
384 }
385
386 pub fn definition(&self, name: &str) -> Option<ToolDefinition> {
387 self.tools
388 .get(name)
389 .map(|tool| self.enriched_definition(tool.as_ref()))
390 }
391
392 pub fn parallelism_for(&self, tool_name: &str) -> ToolParallelism {
393 if EXCLUSIVE_BATCH_TOOL_NAMES.contains(&tool_name) {
394 return ToolParallelism::Exclusive;
395 }
396
397 let Some(definition) = self.definition(tool_name) else {
398 return ToolParallelism::Shared;
399 };
400
401 if definition.metadata.is_read_only && definition.metadata.is_concurrency_safe {
402 ToolParallelism::Shared
403 } else {
404 ToolParallelism::Exclusive
405 }
406 }
407
408 pub fn register_tool(&mut self, tool: Arc<dyn Tool>) -> Option<Arc<dyn Tool>> {
409 let mut def = tool.definition();
410 let name = def.name.clone();
411
412 if def.metadata.is_default() {
414 let builtin = builtin_metadata(&name, def.kind);
415 def.metadata = builtin;
416 }
417
418 self.registry.register(def.clone());
420
421 match jsonschema::validator_for(&def.input_schema) {
422 Ok(v) => {
423 self.validators.insert(name.clone(), Arc::new(v));
424 self.invalid_schemas.remove(&name);
425 }
426 Err(e) => {
427 self.validators.remove(&name);
428 self.invalid_schemas.insert(name.clone(), e.to_string());
429 tracing::warn!(tool = %name, error = %e, "invalid schema");
430 }
431 }
432 self.tools.insert(name, tool)
433 }
434
435 pub fn validate_arguments(
436 &self,
437 inv: &ToolInvocation,
438 ) -> std::result::Result<(), ToolCallInvalid> {
439 let Some(_) = self.definition(&inv.tool_name) else {
440 return Err(ToolCallInvalid::UnknownTool {
441 tool_name: inv.tool_name.clone(),
442 available_tools: self.tool_names(),
443 });
444 };
445 if let Some(e) = self.invalid_schemas.get(&inv.tool_name) {
446 return Err(ToolCallInvalid::InvalidSchema {
447 tool_name: inv.tool_name.clone(),
448 message: e.clone(),
449 });
450 }
451 if let Some(raw) = inv.input.get("raw_arguments").and_then(Value::as_str) {
452 return Err(ToolCallInvalid::MalformedArguments {
453 tool_name: inv.tool_name.clone(),
454 raw_arguments_preview: raw.chars().take(200).collect(),
455 example: self
456 .definition(&inv.tool_name)
457 .map(|d| example_from_schema(&d.input_schema))
458 .unwrap_or(json!({})),
459 });
460 }
461 let Some(v) = self.validators.get(&inv.tool_name) else {
462 return Err(ToolCallInvalid::InvalidSchema {
463 tool_name: inv.tool_name.clone(),
464 message: "missing validator".into(),
465 });
466 };
467 let errors: Vec<String> = v
468 .iter_errors(&inv.input)
469 .take(4)
470 .map(|e| {
471 let p = e.instance_path().to_string();
472 if p.is_empty() {
473 e.to_string()
474 } else {
475 format!("{e} at {p}")
476 }
477 })
478 .collect();
479 if !errors.is_empty() {
480 return Err(ToolCallInvalid::InvalidArguments {
481 tool_name: inv.tool_name.clone(),
482 problems: errors,
483 example: self
484 .definition(&inv.tool_name)
485 .map(|d| example_from_schema(&d.input_schema))
486 .unwrap_or(json!({})),
487 });
488 }
489 Ok(())
490 }
491
492 pub fn tool_names(&self) -> Vec<String> {
493 let mut n: Vec<String> = self.tools.keys().cloned().collect();
494 n.sort();
495 n
496 }
497
498 pub fn unregister_plugin_tools(&mut self) {
499 self.tools.retain(|n, _| !n.starts_with("plugin__"));
500 self.validators.retain(|n, _| !n.starts_with("plugin__"));
501 self.invalid_schemas
502 .retain(|n, _| !n.starts_with("plugin__"));
503 self.registry.unregister_prefix("plugin__");
504 }
505
506 pub fn invalid_tool_result(&self, inv: &ToolInvocation, err: ToolCallInvalid) -> ToolResult {
507 ToolResult {
508 invocation_id: inv.id.clone(),
509 ok: false,
510 output: tool_call_advice(err),
511 }
512 }
513
514 pub fn validate(&self, inv: &ToolInvocation) -> SecurityDecision {
515 if let Err(e) = self.validate_arguments(inv) {
516 return SecurityDecision::Deny(tool_call_advice_message(&e));
517 }
518 let Some(def) = self.definition(&inv.tool_name) else {
519 return SecurityDecision::Deny(format!("unknown `{}`", inv.tool_name));
520 };
521 self.security.validate_tool(&self.policy, &def, inv)
522 }
523
524 pub async fn invoke(&self, invocation: ToolInvocation) -> ToolResult {
525 self.invoke_with_event_tx(invocation, None).await
526 }
527
528 pub async fn invoke_with_event_tx(
529 &self,
530 invocation: ToolInvocation,
531 event_tx: Option<mpsc::UnboundedSender<AgentEvent>>,
532 ) -> ToolResult {
533 self.invoke_with_context_inner(
534 invocation,
535 ToolInvocationContext {
536 event_tx,
537 ..Default::default()
538 },
539 false,
540 )
541 .await
542 }
543
544 pub async fn invoke_approved_with_event_tx(
545 &self,
546 invocation: ToolInvocation,
547 event_tx: Option<mpsc::UnboundedSender<AgentEvent>>,
548 ) -> ToolResult {
549 self.invoke_with_context_inner(
550 invocation,
551 ToolInvocationContext {
552 event_tx,
553 ..Default::default()
554 },
555 true,
556 )
557 .await
558 }
559
560 pub async fn invoke_with_full_context(
561 &self,
562 invocation: ToolInvocation,
563 context: ToolInvocationContext,
564 approval_granted: bool,
565 ) -> ToolResult {
566 self.invoke_with_context_inner(invocation, context, approval_granted)
567 .await
568 }
569
570 async fn invoke_with_context_inner(
571 &self,
572 invocation: ToolInvocation,
573 context: ToolInvocationContext,
574 approval_granted: bool,
575 ) -> ToolResult {
576 let event_tx = context.event_tx.clone();
577 let inv_id = invocation.id.clone();
578 let started = std::time::Instant::now();
579 let invocation = self.policy.normalize_invocation_paths(&invocation);
580 let invocation =
584 recover_misnamed_tool_invocation(&invocation, |name| self.definition(name).is_some())
585 .unwrap_or(invocation);
586 let tool_name = invocation.tool_name.clone();
587
588 let tool_def = self.definition(&invocation.tool_name);
590 let tool_kind = tool_def.as_ref().map(|d| d.kind);
591 let tool_verifier_hint = tool_def
592 .as_ref()
593 .and_then(|d| d.metadata.verifier.as_deref())
594 .map(|v| v.to_string());
595 let capability_event_tx = event_tx.clone();
596 emit_capability_events(
597 capability_event_tx.as_ref(),
598 &invocation,
599 tool_def.as_ref(),
600 CapabilityDecision::Requested,
601 "tool invocation requested",
602 );
603
604 if let Err(e) = self.validate_arguments(&invocation) {
605 return self.invalid_tool_result(&invocation, e);
606 }
607 match self.validate(&invocation) {
608 SecurityDecision::Allow => {}
609 SecurityDecision::NeedsApproval(risk) if approval_granted => {
610 tracing::debug!(tool = %invocation.tool_name, ?risk, "tool approval already granted");
611 }
612 SecurityDecision::NeedsApproval(risk) => {
613 let message = format!(
614 "approval required for tool `{}`: {:?}",
615 invocation.tool_name, risk
616 );
617 emit_capability_events(
618 capability_event_tx.as_ref(),
619 &invocation,
620 tool_def.as_ref(),
621 CapabilityDecision::Denied,
622 &message,
623 );
624 return ToolResult {
625 invocation_id: inv_id,
626 ok: false,
627 output: json!({
628 "error_code": "approval_required",
629 "error": message,
630 "message": message,
631 "recoverable": true,
632 "hint": "Approve the tool request or switch permission mode (AcceptEdits/Auto/Yolo) if this should not require approval.",
633 }),
634 };
635 }
636 SecurityDecision::Deny(r) => {
637 emit_capability_events(
638 capability_event_tx.as_ref(),
639 &invocation,
640 tool_def.as_ref(),
641 CapabilityDecision::Denied,
642 &r,
643 );
644 return ToolResult {
645 invocation_id: inv_id,
646 ok: false,
647 output: json!({
648 "error_code": "security_denied",
649 "error": r,
650 "message": r,
651 "recoverable": true,
652 "hint": "Adjust the path/command or permission mode. Restricted mode keeps a project path jail; YOLO/AcceptEdits allow broader agency.",
653 }),
654 };
655 }
656 }
657 let Some(tool) = self.tools.get(&invocation.tool_name).cloned() else {
658 let message = format!("unknown `{}`", invocation.tool_name);
659 return ToolResult {
660 invocation_id: inv_id,
661 ok: false,
662 output: json!({
663 "error_code": "unknown_tool",
664 "error": message,
665 "message": message,
666 "recoverable": true,
667 "hint": "Use a registered tool name, or call tool_search to discover tools.",
668 }),
669 };
670 };
671 if invocation.tool_name == "tool_search" {
672 return self.invoke_tool_search(invocation);
673 }
674
675 let pre_execution_snapshot = if tool_kind == Some(crate::tool::ToolKind::Write) {
676 let paths = self.snapshot_paths_for_invocation(&invocation);
677 if paths.is_empty() {
678 None
679 } else {
680 Some(crate::sandbox::SandboxManager::create_snapshot(&paths))
681 }
682 } else {
683 None
684 };
685
686 let inv_input = invocation.input.clone();
689
690 let mut result = match tool.invoke_with_context(invocation, context).await {
691 Ok(r) => truncate_tool_result(r),
692 Err(e) => ToolResult {
693 invocation_id: inv_id.clone(),
694 ok: false,
695 output: json!({"error": format!("{e:#}")}),
696 },
697 };
698
699 if result.ok {
701 let should_check = match tool_kind {
702 Some(crate::tool::ToolKind::Write) => true,
703 Some(crate::tool::ToolKind::Command) => true,
704 _ => false,
705 };
706
707 if should_check {
708 let paths = crate::effect::extract_paths(
709 &result,
710 &ToolInvocation {
711 id: inv_id.clone(),
712 tool_name: tool_name.clone(),
713 input: inv_input,
714 },
715 );
716
717 if !paths.is_empty() {
718 let command = None;
719 let decision = self
720 .policy
721 .post_execution_effect_check(&tool_name, &paths, command);
722
723 match decision {
724 PostDecision::Allow => {
725 }
727 PostDecision::Ask(reason) => {
728 tracing::warn!(
729 tool = %tool_name,
730 reason = %reason,
731 "post-execution effect check: ask user"
732 );
733 if let Value::Object(ref mut map) = result.output {
734 map.insert(
735 "effect_warning".to_string(),
736 json!({
737 "decision": "ask",
738 "message": reason,
739 }),
740 );
741 }
742 }
743 PostDecision::Deny(reason) => {
744 tracing::warn!(
745 tool = %tool_name,
746 reason = %reason,
747 "post-execution effect check: denied"
748 );
749 let rollback = pre_execution_snapshot
750 .as_ref()
751 .map(crate::sandbox::SandboxManager::rollback);
752 let (rolled_back, rollback_error) = rollback_outcome(rollback);
753 emit_capability_events(
754 capability_event_tx.as_ref(),
755 &ToolInvocation {
756 id: inv_id.clone(),
757 tool_name: tool_name.clone(),
758 input: json!({}),
759 },
760 tool_def.as_ref(),
761 CapabilityDecision::Violated,
762 &reason,
763 );
764 return ToolResult {
765 invocation_id: inv_id,
766 ok: false,
767 output: json!({
768 "error": reason,
769 "error_code": "effect_denied",
770 "rolled_back": rolled_back,
771 "rollback_error": rollback_error,
772 }),
773 };
774 }
775 PostDecision::Rollback(reason) => {
776 tracing::warn!(
777 tool = %tool_name,
778 reason = %reason,
779 "post-execution effect check: rollback recommended"
780 );
781 let rollback = pre_execution_snapshot
782 .as_ref()
783 .map(crate::sandbox::SandboxManager::rollback);
784 let (rolled_back, rollback_error) = rollback_outcome(rollback);
785 emit_capability_events(
786 capability_event_tx.as_ref(),
787 &ToolInvocation {
788 id: inv_id.clone(),
789 tool_name: tool_name.clone(),
790 input: json!({}),
791 },
792 tool_def.as_ref(),
793 CapabilityDecision::Violated,
794 &reason,
795 );
796 return ToolResult {
797 invocation_id: inv_id,
798 ok: false,
799 output: json!({
800 "error": reason,
801 "error_code": "effect_rollback",
802 "rolled_back": rolled_back,
803 "rollback_error": rollback_error,
804 }),
805 };
806 }
807 }
808 }
809 }
810 }
811
812 emit_capability_events(
813 capability_event_tx.as_ref(),
814 &ToolInvocation {
815 id: inv_id.clone(),
816 tool_name: tool_name.clone(),
817 input: json!({}),
818 },
819 tool_def.as_ref(),
820 if result.ok {
821 CapabilityDecision::Consumed
822 } else {
823 CapabilityDecision::Violated
824 },
825 if result.ok {
826 "tool invocation completed"
827 } else {
828 "tool invocation failed"
829 },
830 );
831
832 if result.ok && tool_kind == Some(crate::tool::ToolKind::Write) {
836 if let Some(verifier_cmd) = tool_verifier_hint {
837 if let Value::Object(ref mut map) = result.output {
838 map.insert(
839 "verifier_hint".to_string(),
840 json!({
841 "suggested": true,
842 "command": verifier_cmd,
843 "message": format!(
844 "After writing, verify with: verifier(action='run', verifier='command', command='{}')",
845 verifier_cmd
846 ),
847 }),
848 );
849 }
850 }
851 }
852
853 tracing::info!(tool = %tool_name, ok = result.ok, dur_ms = started.elapsed().as_millis() as u64, "invoke finished");
854 result
855 }
856
857 fn invoke_tool_search(&self, invocation: ToolInvocation) -> ToolResult {
858 let query = invocation
859 .input
860 .get("query")
861 .and_then(Value::as_str)
862 .unwrap_or_default()
863 .to_string();
864 let max_results = invocation
865 .input
866 .get("max_results")
867 .and_then(Value::as_u64)
868 .unwrap_or(10)
869 .min(50) as usize;
870 let results = self.registry.search(&query, max_results);
871 let results = results
872 .into_iter()
873 .map(model_friendly_definition)
874 .map(|def| {
875 json!({
876 "name": def.name,
877 "description": def.description,
878 "kind": def.kind,
879 "metadata": def.metadata,
880 "input_schema": def.input_schema,
881 })
882 })
883 .collect::<Vec<_>>();
884
885 ToolResult {
886 invocation_id: invocation.id,
887 ok: true,
888 output: json!({
889 "query": query,
890 "results": results,
891 "total": results.len(),
892 "hint": if results.is_empty() {
893 "No tools found. Try broader terms like: code, browser, package, memory, subagent, sandbox, goal."
894 } else {
895 "These tools may be deferred (not always in the schema). Call a returned tool by its `name` with arguments matching `input_schema`."
896 },
897 "power_catalog": [
898 "code / code_edit / ast_search / symbol_*: symbols and structured code nav",
899 "repo_explore: BM25 semantic search",
900 "package_manager: dependency install/add/update",
901 "browser: headless UI testing",
902 "subagent: nested agent",
903 "apply_patch / sandbox / set_goal / history_ops: advanced workflows",
904 ],
905 }),
906 }
907 }
908
909 fn snapshot_paths_for_invocation(
910 &self,
911 invocation: &ToolInvocation,
912 ) -> Vec<std::path::PathBuf> {
913 let mut paths = Vec::new();
914 for key in ["path", "file", "file_path"] {
915 if let Some(path) = invocation.input.get(key).and_then(Value::as_str) {
916 push_unique_snapshot_path(
917 &mut paths,
918 self.policy.resolve_project_path(Path::new(path)),
919 );
920 }
921 }
922
923 if let Some(patch) = invocation.input.get("patch").and_then(Value::as_str) {
924 for path in crate::security::extract_apply_patch_paths(patch) {
925 push_unique_snapshot_path(
926 &mut paths,
927 self.policy.resolve_project_path(Path::new(&path)),
928 );
929 }
930 }
931 if let Some(patches) = invocation.input.get("patches").and_then(Value::as_array) {
932 for patch in patches.iter().filter_map(Value::as_str) {
933 for path in crate::security::extract_apply_patch_paths(patch) {
934 push_unique_snapshot_path(
935 &mut paths,
936 self.policy.resolve_project_path(Path::new(&path)),
937 );
938 }
939 }
940 }
941
942 paths
943 }
944
945 pub async fn list_background_commands(&self) -> Vec<background::BackgroundCommandSnapshot> {
947 let r = self
948 .invoke(ToolInvocation {
949 id: "bg-list".into(),
950 tool_name: "bash".into(),
951 input: json!({"action": "list"}),
952 })
953 .await;
954 r.output
955 .get("tasks")
956 .and_then(|v| v.as_array())
957 .map(|a| {
958 a.iter()
959 .filter_map(background::BackgroundCommandSnapshot::from_json)
960 .collect()
961 })
962 .unwrap_or_default()
963 }
964
965 pub async fn poll_background_command(
967 &self,
968 task_id: &str,
969 ) -> Option<background::BackgroundCommandSnapshot> {
970 let r = self
971 .invoke(ToolInvocation {
972 id: "bg-poll".into(),
973 tool_name: "bash".into(),
974 input: json!({"task_id": task_id}),
975 })
976 .await;
977 if r.ok {
978 background::BackgroundCommandSnapshot::from_json(&r.output)
979 } else {
980 None
981 }
982 }
983
984 pub async fn cancel_background_command(
986 &self,
987 task_id: &str,
988 ) -> Option<background::BackgroundCommandSnapshot> {
989 let r = self
990 .invoke(ToolInvocation {
991 id: "bg-cancel".into(),
992 tool_name: "bash".into(),
993 input: json!({"task_id": task_id, "action": "cancel"}),
994 })
995 .await;
996 if r.ok {
997 background::BackgroundCommandSnapshot::from_json(&r.output)
998 } else {
999 None
1000 }
1001 }
1002
1003 fn register(&mut self, tool: impl Tool + 'static) {
1004 self.register_tool(Arc::new(tool));
1005 }
1006
1007 fn enriched_definition(&self, tool: &dyn Tool) -> ToolDefinition {
1008 let mut def = tool.definition();
1009 if let Some(registered) = self.registry.get(&def.name) {
1010 def.metadata = registered.definition.metadata.clone();
1011 }
1012 def
1013 }
1014
1015 fn register_builtin_tools(&mut self) {
1016 let pr = self.policy.project_root().to_path_buf();
1017 self.register(ReadTool::new(pr.clone())); self.register(ReadTool::alias(pr.clone(), "read")); self.register(SearchTool::new(pr.clone()));
1020 self.register(SearchTool::grep(pr.clone()));
1021 self.register(SearchTool::fs_browser(pr.clone()));
1022 self.register(SearchTool::list_dir(pr.clone()));
1023 self.register(SearchTool::glob(pr.clone()));
1024 self.register(EditTool::new(pr.clone()));
1025 self.register(MultiEditTool::new(pr.clone()));
1026 self.register(WriteTool::new(pr.clone()));
1027 self.register(WriteTool::write_file(pr.clone()));
1028 self.register(WriteTool::apply_patch(pr.clone()));
1029 self.register(BashTool::new(pr.clone()));
1030 self.register(QuestionTool);
1031 self.register(PlanTool::new(self.policy.clone()));
1032 self.register(PackageManagerTool::new(pr.clone()));
1033 self.register(RuntimeInfoTool::new(
1034 self.policy.clone(),
1035 self.harness_profile.clone(),
1036 ));
1037 #[cfg(feature = "code-vfs")]
1038 {
1039 self.register(CodeReadTool::new(self.policy.clone()));
1040 self.register(CodeEditTool::new(self.policy.clone()));
1041 }
1042 self.register(CodeExecTool::new(self.policy.clone()));
1043 self.register(RepoIntelligenceTool::new(
1044 self.policy.clone(),
1045 RepoIntelligenceAction::AstSearch,
1046 ));
1047 self.register(RepoIntelligenceTool::new(
1048 self.policy.clone(),
1049 RepoIntelligenceAction::SymbolGoto,
1050 ));
1051 self.register(RepoIntelligenceTool::new(
1052 self.policy.clone(),
1053 RepoIntelligenceAction::SymbolReferences,
1054 ));
1055 self.register(RepoIntelligenceTool::new(
1056 self.policy.clone(),
1057 RepoIntelligenceAction::DependencyGraph,
1058 ));
1059 self.register(RepoIntelligenceTool::new(
1060 self.policy.clone(),
1061 RepoIntelligenceAction::TestDiscovery,
1062 ));
1063 self.register(RepoIntelligenceTool::new(
1064 self.policy.clone(),
1065 RepoIntelligenceAction::OwnershipChurn,
1066 ));
1067 self.register(InitSessionTool::new(self.policy.clone()));
1068 self.register(MarkFeatureDoneTool::new(self.policy.clone()));
1069 self.register(AppendNoteTool::new(pr.clone()));
1070 self.register(MemoryTool::new(pr.clone()));
1071 self.register(HistoryOpsTool::new(pr.clone()));
1072 self.register(CurrentTimeTool::new());
1073 self.register(SleepTool::new());
1074 self.register(SetGoalTool);
1075 self.register(ContextRemainingTool::new(pr.clone()));
1076 self.register(RequestUserInputTool::new());
1077 self.register(SandboxTool::new(pr.clone()));
1078 let data_dir = self.policy.data_dir().to_path_buf();
1079 self.register(ViewImageTool::new(pr.clone(), data_dir.clone()));
1080 self.register(ViewImageTool::inspect_image(pr.clone(), data_dir));
1081 #[cfg(feature = "browser")]
1082 self.register(BrowserTool::new(pr.clone()));
1083 self.register(NewContextWindowTool::new());
1084 self.register(ToolSearchTool::new(Arc::new(self.registry.clone())));
1085 }
1086}
1087
1088fn tool_call_advice(err: ToolCallInvalid) -> Value {
1089 match err {
1091 ToolCallInvalid::UnknownTool {
1092 tool_name,
1093 available_tools,
1094 } => {
1095 let message = "Requested tool is not registered. Use one of the available tool names."
1096 .to_string();
1097 json!({
1098 "error_code": "unknown_tool",
1099 "error_kind": "unknown_tool",
1100 "tool": tool_name,
1101 "error": message,
1102 "message": message,
1103 "hint": "Call tool_search or use a name from available_tools.",
1104 "recoverable": true,
1105 "suggestions": suggest_tool_replacements(&tool_name, &available_tools),
1106 "available_tools": available_tools.into_iter().take(20).collect::<Vec<_>>(),
1107 })
1108 }
1109 ToolCallInvalid::InvalidSchema { tool_name, message } => {
1110 let message = format!("Tool schema is invalid: {message}");
1111 json!({
1112 "error_code": "invalid_schema",
1113 "error_kind": "invalid_schema",
1114 "tool": tool_name,
1115 "error": message,
1116 "message": message,
1117 "recoverable": false,
1118 })
1119 }
1120 ToolCallInvalid::MalformedArguments {
1121 tool_name,
1122 raw_arguments_preview,
1123 example,
1124 } => {
1125 let message = "Tool arguments were not valid JSON. Emit one complete JSON object matching the schema before calling the tool again.".to_string();
1126 json!({
1127 "error_code": "invalid_arguments",
1128 "error_kind": "malformed_arguments",
1129 "tool": tool_name,
1130 "error": message,
1131 "message": message,
1132 "hint": "Emit a single complete JSON object; do not wrap arguments in markdown fences.",
1133 "recoverable": true,
1134 "raw_arguments_preview": raw_arguments_preview,
1135 "example": example,
1136 })
1137 }
1138 ToolCallInvalid::InvalidArguments {
1139 tool_name,
1140 problems,
1141 example,
1142 } => {
1143 let message = "Tool arguments do not match the JSON schema. Fix the arguments and call the tool again.".to_string();
1144 json!({
1145 "error_code": "invalid_arguments",
1146 "error_kind": "invalid_arguments",
1147 "tool": tool_name,
1148 "error": message,
1149 "message": message,
1150 "hint": "Compare your arguments to the tool schema and the example.",
1151 "recoverable": true,
1152 "problems": problems,
1153 "example": example,
1154 })
1155 }
1156 }
1157}
1158
1159fn tool_call_advice_message(err: &ToolCallInvalid) -> String {
1160 match err {
1161 ToolCallInvalid::UnknownTool { tool_name, .. } => format!("unknown tool `{tool_name}`"),
1162 ToolCallInvalid::InvalidSchema { tool_name, message } => {
1163 format!("invalid schema for `{tool_name}`: {message}")
1164 }
1165 ToolCallInvalid::MalformedArguments { tool_name, .. } => {
1166 format!("malformed args for `{tool_name}`")
1167 }
1168 ToolCallInvalid::InvalidArguments {
1169 tool_name,
1170 problems,
1171 ..
1172 } => format!("invalid args for `{tool_name}`: {}", problems.join("; ")),
1173 }
1174}
1175
1176fn model_friendly_definition(mut definition: ToolDefinition) -> ToolDefinition {
1177 definition.input_schema = simplify_schema_for_model(&definition.input_schema);
1178 definition
1179}
1180
1181fn simplify_schema_for_model(schema: &Value) -> Value {
1182 match schema {
1183 Value::Object(object) => simplify_schema_object_for_model(object),
1184 Value::Array(values) => {
1185 Value::Array(values.iter().map(simplify_schema_for_model).collect())
1186 }
1187 value => value.clone(),
1188 }
1189}
1190
1191fn simplify_schema_object_for_model(object: &Map<String, Value>) -> Value {
1192 let mut simplified = Map::new();
1193
1194 for keyword in ["oneOf", "anyOf", "allOf"] {
1195 let Some(branches) = object.get(keyword).and_then(Value::as_array) else {
1196 continue;
1197 };
1198 if let Some(Value::Object(branch)) = branches.first().map(simplify_schema_for_model) {
1199 simplified.extend(branch);
1200 }
1201 break;
1202 }
1203
1204 for (key, value) in object {
1205 if matches!(key.as_str(), "oneOf" | "anyOf" | "allOf" | "const") {
1206 continue;
1207 }
1208 simplified.insert(key.clone(), simplify_schema_for_model(value));
1209 }
1210
1211 Value::Object(simplified)
1212}
1213
1214fn suggest_tool_replacements(tool_name: &str, available_tools: &[String]) -> Vec<String> {
1215 let lower = tool_name.trim().to_ascii_lowercase();
1216 let candidates: &[&str] = match lower.as_str() {
1217 "list_files" | "ls" | "listdir" | "dir" | "list" | "list_dir" | "find" | "find_files"
1218 | "grep" | "glob" | "fs_browser" => &["search"],
1219 "cat" | "type" | "open" | "view_file" | "read" | "view" => &["read_file"],
1220 "patch" | "str_replace" | "search_replace" | "searchreplace" | "multiedit"
1221 | "multi_edit" | "multi-edit" | "batched_edit" | "apply_patch" => &["edit", "write_file"],
1222 "request_user_input" | "ask_user" | "ask" => &["question"],
1223 "shell" | "run" | "terminal" | "exec" | "sh" | "cmd" | "process" => &["bash"],
1224 "rg" | "ripgrep" | "search_code" => &["search", "code"],
1225 "symbols" | "symbol" | "symbols_overview" | "find_symbol" | "find_references"
1226 | "code_diagnostics" => &["code", "ast_search", "symbol_goto"],
1227 "replace_symbol_body"
1228 | "insert_before_symbol"
1229 | "insert_after_symbol"
1230 | "rename_symbol" => &["code_edit"],
1231 _ if looks_like_filesystem_path(tool_name) => &["read_file", "fs_browser", "grep", "bash"],
1232 _ => &[],
1233 };
1234 candidates
1235 .iter()
1236 .filter(|candidate| available_tools.iter().any(|tool| tool == **candidate))
1237 .map(|candidate| (*candidate).to_string())
1238 .collect()
1239}
1240
1241fn recover_misnamed_tool_invocation(
1244 inv: &ToolInvocation,
1245 has_tool: impl Fn(&str) -> bool,
1246) -> Option<ToolInvocation> {
1247 if has_tool(&inv.tool_name) {
1248 return None;
1249 }
1250
1251 let name = inv.tool_name.trim();
1252 if name.is_empty() {
1253 return None;
1254 }
1255
1256 let input = inv.input.as_object();
1257 let action = input
1258 .and_then(|obj| obj.get("action"))
1259 .and_then(Value::as_str)
1260 .map(|s| s.to_ascii_lowercase());
1261 let has_path_field = input
1262 .and_then(|obj| obj.get("path"))
1263 .and_then(Value::as_str)
1264 .is_some_and(|p| !p.is_empty());
1265 let path_from_input = input
1266 .and_then(|obj| obj.get("path"))
1267 .and_then(Value::as_str)
1268 .map(str::to_string);
1269 let path_like_name = looks_like_filesystem_path(name);
1270
1271 if matches!(
1274 action.as_deref(),
1275 Some("list" | "tree" | "find" | "stat" | "read" | "search")
1276 ) && has_tool("fs_browser")
1277 {
1278 let mut recovered = inv.clone();
1279 recovered.tool_name = "fs_browser".to_string();
1280 if !has_path_field && path_like_name {
1281 if let Some(obj) = recovered.input.as_object_mut() {
1282 obj.insert("path".to_string(), Value::String(name.to_string()));
1283 }
1284 }
1285 tracing::info!(
1286 from = %inv.tool_name,
1287 to = "fs_browser",
1288 "recovered misnamed tool invocation"
1289 );
1290 return Some(recovered);
1291 }
1292
1293 if path_like_name && has_tool("read_file") {
1296 let keys: Vec<&str> = input
1297 .map(|obj| obj.keys().map(String::as_str).collect())
1298 .unwrap_or_default();
1299 let readish = keys.is_empty()
1300 || keys.iter().all(|k| {
1301 matches!(
1302 *k,
1303 "path"
1304 | "offset"
1305 | "limit"
1306 | "start_line"
1307 | "end_line"
1308 | "max_bytes"
1309 | "encoding"
1310 )
1311 });
1312 let writeish = keys
1314 .iter()
1315 .any(|k| matches!(*k, "content" | "patch" | "patches" | "edits" | "new_string"));
1316 if readish && !writeish {
1317 let mut recovered = inv.clone();
1318 recovered.tool_name = "read_file".to_string();
1319 if !has_path_field {
1320 let mut obj = serde_json::Map::new();
1321 obj.insert("path".to_string(), Value::String(name.to_string()));
1322 if let Some(input_obj) = input {
1323 for (k, v) in input_obj {
1324 if k != "path" {
1325 obj.insert(k.clone(), v.clone());
1326 }
1327 }
1328 }
1329 recovered.input = Value::Object(obj);
1330 } else if path_from_input.as_deref() != Some(name)
1331 && path_from_input
1332 .as_deref()
1333 .is_some_and(|p| p == "." || p.is_empty())
1334 {
1335 if let Some(obj) = recovered.input.as_object_mut() {
1337 obj.insert("path".to_string(), Value::String(name.to_string()));
1338 }
1339 }
1340 tracing::info!(
1341 from = %inv.tool_name,
1342 to = "read_file",
1343 "recovered misnamed tool invocation"
1344 );
1345 return Some(recovered);
1346 }
1347 }
1348
1349 None
1350}
1351
1352fn looks_like_filesystem_path(name: &str) -> bool {
1353 let name = name.trim();
1354 if name.is_empty() {
1355 return false;
1356 }
1357 if name == "." || name == ".." {
1358 return true;
1359 }
1360 if name.contains('/') || name.contains('\\') {
1361 return true;
1362 }
1363 if let Some((_, ext)) = name.rsplit_once('.') {
1365 let ext = ext.trim();
1366 if !ext.is_empty()
1367 && ext.len() <= 12
1368 && ext
1369 .chars()
1370 .all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '-')
1371 && !name.starts_with('.')
1372 && (ext.len() <= 8)
1375 {
1376 return true;
1377 }
1378 }
1379 false
1380}
1381
1382fn push_unique_snapshot_path(paths: &mut Vec<std::path::PathBuf>, path: std::path::PathBuf) {
1383 if !paths.contains(&path) {
1384 paths.push(path);
1385 }
1386}
1387
1388fn rollback_outcome(rollback: Option<std::result::Result<(), String>>) -> (bool, Option<String>) {
1389 match rollback {
1390 Some(Ok(())) => (true, None),
1391 Some(Err(error)) => (false, Some(error)),
1392 None => (false, None),
1393 }
1394}
1395
1396fn emit_capability_events(
1397 event_tx: Option<&mpsc::UnboundedSender<AgentEvent>>,
1398 invocation: &ToolInvocation,
1399 definition: Option<&ToolDefinition>,
1400 decision: CapabilityDecision,
1401 justification: &str,
1402) {
1403 let Some(event_tx) = event_tx else {
1404 return;
1405 };
1406 let Some(definition) = definition else {
1407 return;
1408 };
1409 for capability in capabilities_from_tool_metadata(&definition.metadata.capabilities) {
1410 let _ = event_tx.send(AgentEvent::CapabilityRecorded(CapabilityLedgerEntry {
1411 capability,
1412 scope: CapabilityScope::SingleCall(invocation.id.clone()),
1413 decision: decision.clone(),
1414 at_ms: tool_unix_millis(),
1415 justification: format!("{}: {justification}", invocation.tool_name),
1416 }));
1417 }
1418}
1419
1420fn tool_unix_millis() -> u64 {
1421 std::time::SystemTime::now()
1422 .duration_since(std::time::UNIX_EPOCH)
1423 .map(|duration| duration.as_millis() as u64)
1424 .unwrap_or(0)
1425}
1426
1427pub fn example_from_schema(schema: &Value) -> Value {
1428 if let Some(ex) = schema
1429 .get("examples")
1430 .and_then(Value::as_array)
1431 .and_then(|a| a.first())
1432 {
1433 return ex.clone();
1434 }
1435 let Some(properties) = schema.get("properties").and_then(Value::as_object) else {
1436 return json!({});
1437 };
1438 let required: Vec<&str> = schema
1439 .get("required")
1440 .and_then(Value::as_array)
1441 .into_iter()
1442 .flatten()
1443 .filter_map(Value::as_str)
1444 .collect();
1445 let mut ex = serde_json::Map::new();
1446 for field in required {
1447 let v = properties
1448 .get(field)
1449 .and_then(|p| p.get("type"))
1450 .and_then(Value::as_str)
1451 .map(|k| match k {
1452 "integer" => json!(1),
1453 "number" => json!(1.0),
1454 "boolean" => json!(true),
1455 "array" => json!([]),
1456 "object" => json!({}),
1457 _ => json!("example"),
1458 })
1459 .unwrap_or(json!("example"));
1460 ex.insert(field.to_string(), v);
1461 }
1462 Value::Object(ex)
1463}