1use crate::bash_safety::classify_bash_command;
33use crate::file_tracker::FileTracker;
34use crate::tools::ToolEffect;
35use path_clean::PathClean;
36use std::path::Path;
37use std::sync::Arc;
38use std::sync::atomic::{AtomicU8, Ordering};
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
60#[repr(u8)]
61pub enum TrustMode {
62 Plan = 0,
64 #[default]
66 Safe = 1,
67 Auto = 2,
69}
70
71impl TrustMode {
72 pub fn next(self) -> Self {
76 match self {
77 Self::Plan => Self::Plan, Self::Safe => Self::Auto,
79 Self::Auto => Self::Safe,
80 }
81 }
82
83 pub fn as_str(self) -> &'static str {
85 match self {
86 Self::Plan => "plan",
87 Self::Safe => "safe",
88 Self::Auto => "auto",
89 }
90 }
91
92 pub fn label(self) -> &'static str {
94 self.as_str()
95 }
96
97 pub fn description(self) -> &'static str {
99 match self {
100 Self::Plan => "read-only, deny all writes",
101 Self::Safe => "confirm every side effect",
102 Self::Auto => "auto-approve, confirm outside-project only",
103 }
104 }
105
106 pub fn parse(s: &str) -> Option<Self> {
108 match s.to_lowercase().as_str() {
109 "auto" | "yolo" | "accept" => Some(Self::Auto),
110 "safe" | "confirm" | "strict" | "normal" => Some(Self::Safe),
111 "plan" | "readonly" | "read-only" => Some(Self::Plan),
112 _ => None,
113 }
114 }
115
116 pub fn clamp(parent: TrustMode, child: TrustMode) -> TrustMode {
121 std::cmp::min(parent, child)
122 }
123}
124
125pub fn derive_child_trust(parent_runtime: TrustMode, declared: TrustMode) -> TrustMode {
187 TrustMode::clamp(parent_runtime, declared)
193}
194
195impl From<u8> for TrustMode {
196 fn from(v: u8) -> Self {
197 match v {
198 0 => Self::Plan,
199 1 => Self::Safe,
200 2 => Self::Auto,
201 _ => Self::Safe,
204 }
205 }
206}
207
208impl std::fmt::Display for TrustMode {
209 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210 f.write_str(self.as_str())
211 }
212}
213
214pub type SharedTrustMode = Arc<AtomicU8>;
218
219pub fn new_shared_trust(mode: TrustMode) -> SharedTrustMode {
221 Arc::new(AtomicU8::new(mode as u8))
222}
223
224pub fn read_trust(shared: &SharedTrustMode) -> TrustMode {
226 TrustMode::from(shared.load(Ordering::Relaxed))
227}
228
229pub fn set_trust(shared: &SharedTrustMode, mode: TrustMode) {
231 shared.store(mode as u8, Ordering::Relaxed);
232}
233
234pub fn cycle_trust(shared: &SharedTrustMode) -> TrustMode {
236 let current = read_trust(shared);
237 let next = current.next();
238 set_trust(shared, next);
239 next
240}
241
242#[derive(Debug, Clone, PartialEq)]
246pub enum ToolApproval {
247 AutoApprove,
249 NeedsConfirmation,
251 Blocked,
253}
254
255pub fn check_tool(
271 tool_name: &str,
272 args: &serde_json::Value,
273 mode: TrustMode,
274 project_root: Option<&Path>,
275) -> ToolApproval {
276 check_tool_with_tracker(tool_name, args, mode, project_root, None)
277}
278
279pub fn check_tool_with_tracker(
285 tool_name: &str,
286 args: &serde_json::Value,
287 mode: TrustMode,
288 project_root: Option<&Path>,
289 file_tracker: Option<&FileTracker>,
290) -> ToolApproval {
291 let effect = resolve_tool_effect(tool_name, args);
292
293 if effect == ToolEffect::ReadOnly {
295 return ToolApproval::AutoApprove;
296 }
297
298 if mode == TrustMode::Plan {
300 return ToolApproval::Blocked;
301 }
302
303 if let Some(root) = project_root {
305 if is_outside_project(tool_name, args, root) {
306 return ToolApproval::NeedsConfirmation;
307 }
308 if tool_name == "Bash" {
310 let command = args
311 .get("command")
312 .or(args.get("cmd"))
313 .and_then(|v| v.as_str())
314 .unwrap_or("");
315 let lint = crate::bash_path_lint::lint_bash_paths(command, root);
316 if lint.has_warnings() {
317 return ToolApproval::NeedsConfirmation;
318 }
319 }
320 }
321
322 if tool_name == "Delete"
324 && let Some(tracker) = file_tracker
325 && let Some(root) = project_root
326 && let Some(abs_path) = crate::file_tracker::resolve_file_path_from_args(args, root)
327 && tracker.is_owned(&abs_path)
328 {
329 return ToolApproval::AutoApprove;
330 }
331
332 match mode {
334 TrustMode::Plan => unreachable!(), TrustMode::Safe => match effect {
336 ToolEffect::ReadOnly => ToolApproval::AutoApprove,
337 ToolEffect::RemoteAction | ToolEffect::LocalMutation | ToolEffect::Destructive => {
338 ToolApproval::NeedsConfirmation
339 }
340 },
341 TrustMode::Auto => match effect {
342 ToolEffect::ReadOnly => ToolApproval::AutoApprove,
343 ToolEffect::RemoteAction | ToolEffect::LocalMutation | ToolEffect::Destructive => {
344 if crate::sandbox::is_available() {
348 ToolApproval::AutoApprove
349 } else {
350 ToolApproval::NeedsConfirmation
351 }
352 }
353 },
354 }
355}
356
357pub fn resolve_tool_effect(tool_name: &str, args: &serde_json::Value) -> ToolEffect {
365 resolve_tool_effect_inner(tool_name, args, None)
366}
367
368pub fn resolve_tool_effect_with_registry(
371 tool_name: &str,
372 args: &serde_json::Value,
373 registry: &crate::tools::ToolRegistry,
374) -> ToolEffect {
375 resolve_tool_effect_inner(tool_name, args, Some(registry))
376}
377
378fn resolve_tool_effect_inner(
379 tool_name: &str,
380 args: &serde_json::Value,
381 registry: Option<&crate::tools::ToolRegistry>,
382) -> ToolEffect {
383 if crate::mcp::is_mcp_tool_name(tool_name) {
385 if let Some(reg) = registry {
386 return reg.classify_tool_with_mcp(tool_name);
387 }
388 return crate::tools::ToolEffect::RemoteAction;
389 }
390
391 let base = crate::tools::classify_tool(tool_name);
392
393 if tool_name == "Bash" {
394 let command = args
395 .get("command")
396 .or(args.get("cmd"))
397 .and_then(|v| v.as_str())
398 .unwrap_or("");
399 return classify_bash_command(command);
400 }
401
402 base
403}
404
405fn is_outside_project(tool_name: &str, args: &serde_json::Value, project_root: &Path) -> bool {
410 let path_arg = match tool_name {
411 "Write" | "Edit" | "Delete" => args
412 .get("path")
413 .or(args.get("file_path"))
414 .and_then(|v| v.as_str()),
415 _ => None,
416 };
417 match path_arg {
418 Some(p) => {
419 let requested = Path::new(p);
420 let abs_path = if requested.is_absolute() {
421 requested.to_path_buf()
422 } else {
423 project_root.join(requested)
424 };
425 let resolved = abs_path.canonicalize().unwrap_or_else(|_| {
428 if let Some(parent) = abs_path.parent()
429 && let Ok(canon_parent) = parent.canonicalize()
430 && let Some(name) = abs_path.file_name()
431 {
432 return canon_parent.join(name);
433 }
434 abs_path.clean()
435 });
436 let canon_root = project_root
437 .canonicalize()
438 .unwrap_or_else(|_| project_root.to_path_buf());
439 let outside = !resolved.starts_with(&canon_root);
440 if outside && crate::bash_path_lint::is_safe_external_path(&resolved) {
442 return false;
443 }
444 outside
445 }
446 None => false,
447 }
448}
449
450pub use crate::last_provider::LastProvider;
454
455#[cfg(test)]
458mod tests {
459 use super::*;
460
461 #[test]
464 fn test_mode_cycle() {
465 assert_eq!(TrustMode::Safe.next(), TrustMode::Auto);
466 assert_eq!(TrustMode::Auto.next(), TrustMode::Safe);
467 assert_eq!(TrustMode::Plan.next(), TrustMode::Plan); }
469
470 #[test]
471 fn test_mode_ordering() {
472 assert!(TrustMode::Plan < TrustMode::Safe);
473 assert!(TrustMode::Safe < TrustMode::Auto);
474 }
475
476 #[test]
477 fn test_clamp() {
478 assert_eq!(
479 TrustMode::clamp(TrustMode::Plan, TrustMode::Auto),
480 TrustMode::Plan
481 );
482 assert_eq!(
483 TrustMode::clamp(TrustMode::Safe, TrustMode::Auto),
484 TrustMode::Safe
485 );
486 assert_eq!(
487 TrustMode::clamp(TrustMode::Auto, TrustMode::Safe),
488 TrustMode::Safe
489 );
490 assert_eq!(
491 TrustMode::clamp(TrustMode::Auto, TrustMode::Auto),
492 TrustMode::Auto
493 );
494 }
495
496 #[test]
513 fn derive_child_trust_fork_identity() {
514 for parent in [TrustMode::Plan, TrustMode::Safe, TrustMode::Auto] {
517 assert_eq!(
518 derive_child_trust(parent, parent),
519 parent,
520 "fork (parent==declared) must return parent verbatim"
521 );
522 }
523 }
524
525 #[test]
526 fn derive_child_trust_named_clamps_down() {
527 assert_eq!(
532 derive_child_trust(TrustMode::Safe, TrustMode::Auto),
533 TrustMode::Safe
534 );
535 assert_eq!(
536 derive_child_trust(TrustMode::Plan, TrustMode::Auto),
537 TrustMode::Plan
538 );
539 assert_eq!(
540 derive_child_trust(TrustMode::Plan, TrustMode::Safe),
541 TrustMode::Plan
542 );
543 }
544
545 #[test]
546 fn derive_child_trust_child_already_stricter_passes_through() {
547 assert_eq!(
550 derive_child_trust(TrustMode::Auto, TrustMode::Plan),
551 TrustMode::Plan
552 );
553 assert_eq!(
554 derive_child_trust(TrustMode::Auto, TrustMode::Safe),
555 TrustMode::Safe
556 );
557 assert_eq!(
558 derive_child_trust(TrustMode::Safe, TrustMode::Plan),
559 TrustMode::Plan
560 );
561 }
562
563 #[test]
564 fn derive_child_trust_is_commutative_in_min_but_not_in_meaning() {
565 for a in [TrustMode::Plan, TrustMode::Safe, TrustMode::Auto] {
575 for b in [TrustMode::Plan, TrustMode::Safe, TrustMode::Auto] {
576 assert_eq!(derive_child_trust(a, b), derive_child_trust(b, a));
577 }
578 }
579 }
580
581 #[test]
582 fn test_mode_from_str() {
583 assert_eq!(TrustMode::parse("auto"), Some(TrustMode::Auto));
584 assert_eq!(TrustMode::parse("safe"), Some(TrustMode::Safe));
585 assert_eq!(TrustMode::parse("plan"), Some(TrustMode::Plan));
586 assert_eq!(TrustMode::parse("yolo"), Some(TrustMode::Auto));
588 assert_eq!(TrustMode::parse("confirm"), Some(TrustMode::Safe));
589 assert_eq!(TrustMode::parse("strict"), Some(TrustMode::Safe));
590 assert_eq!(TrustMode::parse("normal"), Some(TrustMode::Safe));
591 assert_eq!(TrustMode::parse("readonly"), Some(TrustMode::Plan));
592 assert_eq!(TrustMode::parse("read-only"), Some(TrustMode::Plan));
593 assert_eq!(TrustMode::parse("accept"), Some(TrustMode::Auto));
594 assert_eq!(TrustMode::parse("nope"), None);
595 }
596
597 #[test]
598 fn test_mode_from_u8() {
599 assert_eq!(TrustMode::from(0), TrustMode::Plan);
600 assert_eq!(TrustMode::from(1), TrustMode::Safe);
601 assert_eq!(TrustMode::from(2), TrustMode::Auto);
602 assert_eq!(TrustMode::from(99), TrustMode::Safe); }
604
605 #[test]
606 fn test_shared_trust_cycle() {
607 let shared = new_shared_trust(TrustMode::Safe);
608 assert_eq!(read_trust(&shared), TrustMode::Safe);
609 let next = cycle_trust(&shared);
610 assert_eq!(next, TrustMode::Auto);
611 assert_eq!(read_trust(&shared), TrustMode::Auto);
612 }
613
614 #[test]
615 fn test_display() {
616 assert_eq!(TrustMode::Plan.to_string(), "plan");
617 assert_eq!(TrustMode::Safe.to_string(), "safe");
618 assert_eq!(TrustMode::Auto.to_string(), "auto");
619 }
620
621 #[test]
622 fn test_default() {
623 assert_eq!(TrustMode::default(), TrustMode::Safe);
624 }
625
626 const READ_ONLY_TOOLS: &[&str] = &[
629 "Read",
630 "List",
631 "Grep",
632 "Glob",
633 "MemoryRead",
634 "ListAgents",
635 "InvokeAgent",
636 "WebFetch",
637 "WebSearch",
638 "ListSkills",
639 "ActivateSkill",
640 ];
641
642 #[test]
643 fn test_read_tools_always_approved() {
644 for tool in READ_ONLY_TOOLS {
645 for mode in [TrustMode::Plan, TrustMode::Safe, TrustMode::Auto] {
646 assert_eq!(
647 check_tool(tool, &serde_json::json!({}), mode, None),
648 ToolApproval::AutoApprove,
649 "{tool} should auto-approve in {mode:?}"
650 );
651 }
652 }
653 }
654
655 #[test]
656 fn test_plan_blocks_all_writes() {
657 for tool in ["Write", "Edit", "Delete", "MemoryWrite", "TodoWrite"] {
658 assert_eq!(
659 check_tool(tool, &serde_json::json!({}), TrustMode::Plan, None),
660 ToolApproval::Blocked,
661 "{tool} should be blocked in Plan mode"
662 );
663 }
664 }
665
666 #[test]
667 fn test_safe_confirms_writes() {
668 for tool in ["Write", "Edit", "Delete", "MemoryWrite", "TodoWrite"] {
669 assert_eq!(
670 check_tool(tool, &serde_json::json!({}), TrustMode::Safe, None),
671 ToolApproval::NeedsConfirmation,
672 "{tool} should need confirmation in Safe mode"
673 );
674 }
675 }
676
677 #[test]
678 fn test_auto_approves_non_outside() {
679 let expected = if crate::sandbox::is_available() {
682 ToolApproval::AutoApprove
683 } else {
684 ToolApproval::NeedsConfirmation
685 };
686 for tool in ["Write", "Edit", "TodoWrite"] {
687 assert_eq!(
688 check_tool(tool, &serde_json::json!({}), TrustMode::Auto, None),
689 expected,
690 "{tool} in Auto mode"
691 );
692 }
693 assert_eq!(
695 check_tool("WebFetch", &serde_json::json!({}), TrustMode::Auto, None),
696 ToolApproval::AutoApprove,
697 );
698 }
699
700 #[test]
701 fn test_auto_approves_destructive() {
702 let expected = if crate::sandbox::is_available() {
705 ToolApproval::AutoApprove
706 } else {
707 ToolApproval::NeedsConfirmation
708 };
709 assert_eq!(
710 check_tool("Delete", &serde_json::json!({}), TrustMode::Auto, None),
711 expected,
712 );
713 }
714
715 #[test]
716 fn test_safe_bash_read_only_auto_approved() {
717 let args = serde_json::json!({"command": "git status"});
718 assert_eq!(
719 check_tool("Bash", &args, TrustMode::Safe, None),
720 ToolApproval::AutoApprove,
721 );
722 }
723
724 #[test]
726 fn test_gh_read_only_auto_approved() {
727 for cmd in [
728 "gh issue view 42",
729 "gh pr view 99",
730 "gh pr list",
731 "gh issue list",
732 ] {
733 let args = serde_json::json!({"command": cmd});
734 assert_eq!(
735 check_tool("Bash", &args, TrustMode::Safe, None),
736 ToolApproval::AutoApprove,
737 "{cmd} should auto-approve even in Safe mode"
738 );
739 }
740 }
741
742 #[test]
744 fn test_gh_destructive_needs_confirmation_in_safe() {
745 for cmd in [
746 "gh pr merge 42 --squash",
747 "gh issue delete 42",
748 "gh repo delete owner/repo",
749 ] {
750 let args = serde_json::json!({"command": cmd});
751 assert_eq!(
752 check_tool("Bash", &args, TrustMode::Safe, None),
753 ToolApproval::NeedsConfirmation,
754 "{cmd} should need confirmation in Safe mode"
755 );
756 }
757 }
758
759 #[test]
761 fn test_gh_mutation_auto_approved_in_auto() {
762 for cmd in [
763 "gh issue create --title 'bug'",
764 "gh issue edit 42",
765 "gh pr create",
766 ] {
767 let args = serde_json::json!({"command": cmd});
768 assert_eq!(
769 check_tool("Bash", &args, TrustMode::Auto, None),
770 ToolApproval::AutoApprove,
771 "{cmd} should auto-approve in Auto mode"
772 );
773 assert_eq!(
774 check_tool("Bash", &args, TrustMode::Safe, None),
775 ToolApproval::NeedsConfirmation,
776 "{cmd} should need confirmation in Safe mode"
777 );
778 }
779 }
780
781 #[test]
782 fn test_dev_workflow_bash_needs_confirmation_in_safe() {
783 let args = serde_json::json!({"command": "cargo test --release"});
784 assert_eq!(
785 check_tool("Bash", &args, TrustMode::Safe, None),
786 ToolApproval::NeedsConfirmation,
787 );
788 }
789
790 #[test]
791 fn test_dangerous_bash_needs_confirmation_in_safe() {
792 let args = serde_json::json!({"command": "rm -rf target/"});
793 assert_eq!(
794 check_tool("Bash", &args, TrustMode::Safe, None),
795 ToolApproval::NeedsConfirmation,
796 );
797 }
798
799 #[test]
800 fn test_plan_blocks_bash() {
801 let args = serde_json::json!({"command": "cargo test"});
802 assert_eq!(
803 check_tool("Bash", &args, TrustMode::Plan, None),
804 ToolApproval::Blocked,
805 );
806 }
807
808 #[test]
809 fn test_invoke_agent_auto_approved() {
810 let args = serde_json::json!({"agent_name": "reviewer", "prompt": "review this"});
811 for mode in [TrustMode::Plan, TrustMode::Safe, TrustMode::Auto] {
812 assert_eq!(
813 check_tool("InvokeAgent", &args, mode, None),
814 ToolApproval::AutoApprove,
815 );
816 }
817 }
818
819 #[test]
822 fn test_write_outside_project_needs_confirmation() {
823 let root = Path::new("/home/user/project");
824 let args = serde_json::json!({"path": "/etc/hosts"});
825 assert_eq!(
826 check_tool("Write", &args, TrustMode::Auto, Some(root)),
827 ToolApproval::NeedsConfirmation,
828 );
829 }
830
831 #[test]
832 fn test_write_inside_project_auto_approved() {
833 let root = Path::new("/home/user/project");
834 let args = serde_json::json!({"path": "src/main.rs"});
835 assert_eq!(
836 check_tool("Write", &args, TrustMode::Auto, Some(root)),
837 ToolApproval::AutoApprove,
838 );
839 }
840
841 #[test]
842 fn test_edit_with_dotdot_escape_needs_confirmation() {
843 let root = Path::new("/home/user/project");
844 let args = serde_json::json!({"path": "../../../etc/passwd"});
845 assert_eq!(
846 check_tool("Edit", &args, TrustMode::Auto, Some(root)),
847 ToolApproval::NeedsConfirmation,
848 );
849 }
850
851 #[test]
852 fn test_bash_cd_outside_needs_confirmation() {
853 let root = Path::new("/home/user/project");
854 let args = serde_json::json!({"command": "cd /etc && ls"});
855 assert_eq!(
856 check_tool("Bash", &args, TrustMode::Auto, Some(root)),
857 ToolApproval::NeedsConfirmation,
858 );
859 }
860
861 #[test]
862 fn test_bash_cd_inside_auto_approved() {
863 let root = Path::new("/home/user/project");
864 let args = serde_json::json!({"command": "cd src && ls"});
865 assert_eq!(
866 check_tool("Bash", &args, TrustMode::Auto, Some(root)),
867 ToolApproval::AutoApprove,
868 );
869 }
870
871 #[test]
872 fn test_no_project_root_skips_path_check() {
873 let args = serde_json::json!({"path": "/etc/hosts"});
874 assert_eq!(
875 check_tool("Write", &args, TrustMode::Auto, None),
876 ToolApproval::AutoApprove,
877 );
878 }
879
880 #[test]
883 fn test_write_to_tmp_auto_approved() {
884 let root = Path::new("/home/user/project");
885 let args = serde_json::json!({"path": "/tmp/issue-draft.md"});
886 assert_eq!(
887 check_tool("Write", &args, TrustMode::Auto, Some(root)),
888 ToolApproval::AutoApprove,
889 "/tmp writes should auto-approve"
890 );
891 }
892
893 #[test]
894 fn test_bash_cd_tmp_auto_approved() {
895 let root = Path::new("/home/user/project");
896 let args = serde_json::json!({"command": "cd /tmp && ls"});
897 assert_eq!(
898 check_tool("Bash", &args, TrustMode::Auto, Some(root)),
899 ToolApproval::AutoApprove,
900 "cd /tmp should auto-approve"
901 );
902 }
903
904 #[test]
905 fn test_write_to_etc_still_blocked() {
906 let root = Path::new("/home/user/project");
907 let args = serde_json::json!({"path": "/etc/hosts"});
908 assert_eq!(
909 check_tool("Write", &args, TrustMode::Auto, Some(root)),
910 ToolApproval::NeedsConfirmation,
911 "/etc writes should still need confirmation"
912 );
913 }
914
915 #[tokio::test]
918 async fn test_delete_owned_file_auto_approved() {
919 let dir = tempfile::TempDir::new().unwrap();
920 let db = crate::db::Database::open(&dir.path().join("test.db"))
921 .await
922 .unwrap();
923 let mut tracker = FileTracker::new("test-sess", db).await;
924 let root = Path::new("/home/user/project");
925 let owned_path = root.join("temp_output.md");
926 tracker.track_created(owned_path).await;
927
928 let args = serde_json::json!({"path": "temp_output.md"});
929 assert_eq!(
930 check_tool_with_tracker("Delete", &args, TrustMode::Auto, Some(root), Some(&tracker),),
931 ToolApproval::AutoApprove,
932 "Delete of Koda-owned file should auto-approve"
933 );
934 }
935
936 #[tokio::test]
937 async fn test_delete_unowned_file_needs_confirmation_in_safe() {
938 let dir = tempfile::TempDir::new().unwrap();
939 let db = crate::db::Database::open(&dir.path().join("test.db"))
940 .await
941 .unwrap();
942 let tracker = FileTracker::new("test-sess", db).await;
943 let root = Path::new("/home/user/project");
944
945 let args = serde_json::json!({"path": "user_file.rs"});
946 assert_eq!(
947 check_tool_with_tracker("Delete", &args, TrustMode::Safe, Some(root), Some(&tracker),),
948 ToolApproval::NeedsConfirmation,
949 "Delete of unowned file should need confirmation in Safe mode"
950 );
951 }
952
953 #[tokio::test]
954 async fn test_delete_owned_file_safe_mode_auto_approved() {
955 let dir = tempfile::TempDir::new().unwrap();
956 let db = crate::db::Database::open(&dir.path().join("test.db"))
957 .await
958 .unwrap();
959 let mut tracker = FileTracker::new("test-sess", db).await;
960 let root = Path::new("/home/user/project");
961 let owned_path = root.join("scratch.txt");
962 tracker.track_created(owned_path).await;
963
964 let args = serde_json::json!({"path": "scratch.txt"});
965 assert_eq!(
966 check_tool_with_tracker("Delete", &args, TrustMode::Safe, Some(root), Some(&tracker),),
967 ToolApproval::AutoApprove,
968 "Delete of Koda-owned file should auto-approve even in Safe mode"
969 );
970 }
971
972 #[test]
973 fn test_no_tracker_safe_mode_delete_needs_confirmation() {
974 let root = Path::new("/home/user/project");
975 let args = serde_json::json!({"path": "some_file.rs"});
976 assert_eq!(
977 check_tool_with_tracker("Delete", &args, TrustMode::Safe, Some(root), None),
978 ToolApproval::NeedsConfirmation,
979 "Without tracker, Delete should need confirmation in Safe"
980 );
981 }
982}