tool_permissions_setup.rs

  1use agent::{HARDCODED_SECURITY_RULES, ToolPermissionDecision};
  2use agent_settings::AgentSettings;
  3use gpui::{Focusable, ReadGlobal, ScrollHandle, TextStyleRefinement, point, prelude::*};
  4use settings::{Settings as _, SettingsStore, ToolPermissionMode};
  5use std::sync::Arc;
  6use theme::ThemeSettings;
  7use ui::{Banner, ContextMenu, Divider, PopoverMenu, Tooltip, prelude::*};
  8use util::shell::ShellKind;
  9
 10use crate::{SettingsWindow, components::SettingsInputField};
 11
 12/// Tools that support permission rules
 13const TOOLS: &[ToolInfo] = &[
 14    ToolInfo {
 15        id: "terminal",
 16        name: "Terminal",
 17        description: "Commands executed in the terminal",
 18        regex_explanation: "Patterns are matched against each command in the input. Commands chained with &&, ||, ;, or pipes are split and checked individually.",
 19    },
 20    ToolInfo {
 21        id: "edit_file",
 22        name: "Edit File",
 23        description: "File editing operations",
 24        regex_explanation: "Patterns are matched against the file path being edited.",
 25    },
 26    ToolInfo {
 27        id: "delete_path",
 28        name: "Delete Path",
 29        description: "File and directory deletion",
 30        regex_explanation: "Patterns are matched against the path being deleted.",
 31    },
 32    ToolInfo {
 33        id: "move_path",
 34        name: "Move Path",
 35        description: "File and directory moves/renames",
 36        regex_explanation: "Patterns are matched against both the source and destination paths.",
 37    },
 38    ToolInfo {
 39        id: "create_directory",
 40        name: "Create Directory",
 41        description: "Directory creation",
 42        regex_explanation: "Patterns are matched against the directory path being created.",
 43    },
 44    ToolInfo {
 45        id: "save_file",
 46        name: "Save File",
 47        description: "File saving operations",
 48        regex_explanation: "Patterns are matched against the file path being saved.",
 49    },
 50    ToolInfo {
 51        id: "fetch",
 52        name: "Fetch",
 53        description: "HTTP requests to URLs",
 54        regex_explanation: "Patterns are matched against the URL being fetched.",
 55    },
 56    ToolInfo {
 57        id: "web_search",
 58        name: "Web Search",
 59        description: "Web search queries",
 60        regex_explanation: "Patterns are matched against the search query.",
 61    },
 62];
 63
 64struct ToolInfo {
 65    id: &'static str,
 66    name: &'static str,
 67    description: &'static str,
 68    regex_explanation: &'static str,
 69}
 70
 71/// Renders the main tool permissions setup page showing a list of tools
 72pub(crate) fn render_tool_permissions_setup_page(
 73    settings_window: &SettingsWindow,
 74    scroll_handle: &ScrollHandle,
 75    window: &mut Window,
 76    cx: &mut Context<SettingsWindow>,
 77) -> AnyElement {
 78    let tool_items: Vec<AnyElement> = TOOLS
 79        .iter()
 80        .enumerate()
 81        .map(|(i, tool)| render_tool_list_item(settings_window, tool, i, window, cx))
 82        .collect();
 83
 84    let page_description =
 85        "Configure regex patterns to control which tool actions require confirmation.";
 86
 87    let scroll_step = px(40.);
 88
 89    v_flex()
 90        .id("tool-permissions-page")
 91        .on_action({
 92            let scroll_handle = scroll_handle.clone();
 93            move |_: &menu::SelectNext, window, cx| {
 94                window.focus_next(cx);
 95                let current_offset = scroll_handle.offset();
 96                scroll_handle.set_offset(point(current_offset.x, current_offset.y - scroll_step));
 97            }
 98        })
 99        .on_action({
100            let scroll_handle = scroll_handle.clone();
101            move |_: &menu::SelectPrevious, window, cx| {
102                window.focus_prev(cx);
103                let current_offset = scroll_handle.offset();
104                scroll_handle.set_offset(point(current_offset.x, current_offset.y + scroll_step));
105            }
106        })
107        .min_w_0()
108        .size_full()
109        .pt_2p5()
110        .px_8()
111        .pb_16()
112        .overflow_y_scroll()
113        .track_scroll(scroll_handle)
114        .child(Label::new("Tool Permission Rules").size(LabelSize::Large))
115        .child(
116            Label::new(page_description)
117                .size(LabelSize::Small)
118                .color(Color::Muted),
119        )
120        .child(
121            v_flex()
122                .mt_4()
123                .children(tool_items.into_iter().enumerate().flat_map(|(i, item)| {
124                    let mut elements: Vec<AnyElement> = vec![item];
125                    if i + 1 < TOOLS.len() {
126                        elements.push(Divider::horizontal().into_any_element());
127                    }
128                    elements
129                })),
130        )
131        .into_any_element()
132}
133
134fn render_tool_list_item(
135    _settings_window: &SettingsWindow,
136    tool: &'static ToolInfo,
137    tool_index: usize,
138    _window: &mut Window,
139    cx: &mut Context<SettingsWindow>,
140) -> AnyElement {
141    let rules = get_tool_rules(tool.id, cx);
142    let rule_count =
143        rules.always_allow.len() + rules.always_deny.len() + rules.always_confirm.len();
144
145    let rule_summary = if rule_count > 0 {
146        Some(format!("{} rules", rule_count))
147    } else {
148        None
149    };
150
151    let render_fn = get_tool_render_fn(tool.id);
152
153    h_flex()
154        .w_full()
155        .py_3()
156        .justify_between()
157        .child(
158            v_flex()
159                .child(h_flex().gap_1().child(Label::new(tool.name)).when_some(
160                    rule_summary,
161                    |this, summary| {
162                        this.child(
163                            Label::new(summary)
164                                .size(LabelSize::Small)
165                                .color(Color::Muted),
166                        )
167                    },
168                ))
169                .child(
170                    Label::new(tool.description)
171                        .size(LabelSize::Small)
172                        .color(Color::Muted),
173                ),
174        )
175        .child({
176            let tool_name = tool.name;
177            Button::new(format!("configure-{}", tool.id), "Configure")
178                .tab_index(tool_index as isize)
179                .style(ButtonStyle::OutlinedGhost)
180                .size(ButtonSize::Medium)
181                .icon(IconName::ChevronRight)
182                .icon_position(IconPosition::End)
183                .icon_color(Color::Muted)
184                .icon_size(IconSize::Small)
185                .on_click(cx.listener(move |this, _, window, cx| {
186                    this.push_dynamic_sub_page(
187                        tool_name,
188                        "Configure Tool Rules",
189                        None,
190                        render_fn,
191                        window,
192                        cx,
193                    );
194                }))
195        })
196        .into_any_element()
197}
198
199fn get_tool_render_fn(
200    tool_id: &str,
201) -> fn(&SettingsWindow, &ScrollHandle, &mut Window, &mut Context<SettingsWindow>) -> AnyElement {
202    match tool_id {
203        "terminal" => render_terminal_tool_config,
204        "edit_file" => render_edit_file_tool_config,
205        "delete_path" => render_delete_path_tool_config,
206        "move_path" => render_move_path_tool_config,
207        "create_directory" => render_create_directory_tool_config,
208        "save_file" => render_save_file_tool_config,
209        "fetch" => render_fetch_tool_config,
210        "web_search" => render_web_search_tool_config,
211        _ => render_terminal_tool_config, // fallback
212    }
213}
214
215/// Renders an individual tool's permission configuration page
216pub(crate) fn render_tool_config_page(
217    tool_id: &'static str,
218    _settings_window: &SettingsWindow,
219    scroll_handle: &ScrollHandle,
220    window: &mut Window,
221    cx: &mut Context<SettingsWindow>,
222) -> AnyElement {
223    let tool = TOOLS.iter().find(|t| t.id == tool_id).unwrap();
224    let rules = get_tool_rules(tool_id, cx);
225    let page_title = format!("{} Tool", tool.name);
226    let scroll_step = px(80.);
227
228    v_flex()
229        .id(format!("tool-config-page-{}", tool_id))
230        .on_action({
231            let scroll_handle = scroll_handle.clone();
232            move |_: &menu::SelectNext, window, cx| {
233                window.focus_next(cx);
234                let current_offset = scroll_handle.offset();
235                scroll_handle.set_offset(point(current_offset.x, current_offset.y - scroll_step));
236            }
237        })
238        .on_action({
239            let scroll_handle = scroll_handle.clone();
240            move |_: &menu::SelectPrevious, window, cx| {
241                window.focus_prev(cx);
242                let current_offset = scroll_handle.offset();
243                scroll_handle.set_offset(point(current_offset.x, current_offset.y + scroll_step));
244            }
245        })
246        .min_w_0()
247        .size_full()
248        .pt_2p5()
249        .px_8()
250        .pb_16()
251        .overflow_y_scroll()
252        .track_scroll(scroll_handle)
253        .child(
254            v_flex()
255                .min_w_0()
256                .child(Label::new(page_title).size(LabelSize::Large))
257                .child(
258                    Label::new(tool.regex_explanation)
259                        .size(LabelSize::Small)
260                        .color(Color::Muted),
261                ),
262        )
263        .when(tool_id == "terminal", |this| {
264            this.child(render_hardcoded_security_banner(cx))
265        })
266        .child(render_verification_section(tool_id, window, cx))
267        .child(
268            v_flex()
269                .mt_6()
270                .min_w_0()
271                .w_full()
272                .gap_5()
273                .child(render_default_mode_section(tool_id, rules.default_mode, cx))
274                .child(Divider::horizontal().color(ui::DividerColor::BorderFaded))
275                .child(render_rule_section(
276                    tool_id,
277                    "Always Deny",
278                    "If any of these regexes match, the tool action will be denied.",
279                    RuleType::Deny,
280                    &rules.always_deny,
281                    cx,
282                ))
283                .child(Divider::horizontal().color(ui::DividerColor::BorderFaded))
284                .child(render_rule_section(
285                    tool_id,
286                    "Always Allow",
287                    "If any of these regexes match, the tool action will be approved—unless an Always Confirm or Always Deny regex matches.",
288                    RuleType::Allow,
289                    &rules.always_allow,
290                    cx,
291                ))
292                .child(Divider::horizontal().color(ui::DividerColor::BorderFaded))
293                .child(render_rule_section(
294                    tool_id,
295                    "Always Confirm",
296                    "If any of these regexes match, a confirmation will be shown unless an Always Deny regex matches.",
297                    RuleType::Confirm,
298                    &rules.always_confirm,
299                    cx,
300                )),
301        )
302        .into_any_element()
303}
304
305fn render_hardcoded_security_banner(cx: &mut Context<SettingsWindow>) -> AnyElement {
306    let pattern_labels = HARDCODED_SECURITY_RULES.terminal_deny.iter().map(|rule| {
307        h_flex()
308            .gap_1()
309            .child(
310                Icon::new(IconName::Dash)
311                    .color(Color::Hidden)
312                    .size(IconSize::Small),
313            )
314            .child(
315                Label::new(rule.pattern.clone())
316                    .size(LabelSize::Small)
317                    .color(Color::Muted)
318                    .buffer_font(cx),
319            )
320    });
321
322    v_flex()
323        .mt_3()
324        .child(
325            Banner::new().child(
326                v_flex()
327                    .py_1()
328                    .gap_1()
329                    .child(
330                        Label::new(
331                            "The following patterns are always blocked and cannot be overridden:",
332                        )
333                        .size(LabelSize::Small),
334                    )
335                    .children(pattern_labels),
336            ),
337        )
338        .into_any_element()
339}
340
341fn render_verification_section(
342    tool_id: &'static str,
343    window: &mut Window,
344    cx: &mut Context<SettingsWindow>,
345) -> AnyElement {
346    let input_id = format!("{}-verification-input", tool_id);
347
348    let settings = AgentSettings::get_global(cx);
349    let always_allow_enabled = settings.always_allow_tool_actions;
350
351    let editor = window.use_keyed_state(input_id, cx, |window, cx| {
352        let mut editor = editor::Editor::single_line(window, cx);
353        editor.set_placeholder_text("Enter a rule to see how it applies…", window, cx);
354
355        let global_settings = ThemeSettings::get_global(cx);
356        editor.set_text_style_refinement(TextStyleRefinement {
357            font_family: Some(global_settings.buffer_font.family.clone()),
358            font_size: Some(rems(0.75).into()),
359            ..Default::default()
360        });
361
362        editor
363    });
364
365    cx.observe(&editor, |_, _, cx| cx.notify()).detach();
366
367    let focus_handle = editor.focus_handle(cx).tab_index(0).tab_stop(true);
368
369    let current_text = editor.read(cx).text(cx);
370    let (decision, matched_patterns) = if current_text.is_empty() {
371        (None, Vec::new())
372    } else {
373        let matches = find_matched_patterns(tool_id, &current_text, cx);
374        let decision = evaluate_test_input(tool_id, &current_text, cx);
375        (Some(decision), matches)
376    };
377
378    let always_allow_description = "The Always Allow Tool Actions setting is enabled: all tools will be allowed regardless of these rules.";
379    let theme_colors = cx.theme().colors();
380
381    v_flex()
382        .mt_3()
383        .min_w_0()
384        .gap_2()
385        .when(always_allow_enabled, |this| {
386            this.child(
387                Banner::new()
388                    .severity(Severity::Warning)
389                    .wrap_content(false)
390                    .child(
391                        Label::new(always_allow_description)
392                            .size(LabelSize::Small)
393                            .mt(px(3.))
394                            .mr_8(),
395                    )
396                    .action_slot(
397                        Button::new("configure_setting", "Configure Setting")
398                            .label_size(LabelSize::Small)
399                            .on_click(cx.listener(|this, _, window, cx| {
400                                this.navigate_to_setting(
401                                    "agent.always_allow_tool_actions",
402                                    window,
403                                    cx,
404                                );
405                            })),
406                    ),
407            )
408        })
409        .child(
410            v_flex()
411                .p_2p5()
412                .gap_1p5()
413                .bg(theme_colors.surface_background.opacity(0.15))
414                .border_1()
415                .border_dashed()
416                .border_color(theme_colors.border_variant)
417                .rounded_sm()
418                .child(
419                    Label::new("Test Your Rules")
420                        .color(Color::Muted)
421                        .size(LabelSize::Small),
422                )
423                .child(
424                    h_flex()
425                        .w_full()
426                        .h_8()
427                        .px_2()
428                        .rounded_md()
429                        .border_1()
430                        .border_color(theme_colors.border)
431                        .bg(theme_colors.editor_background)
432                        .track_focus(&focus_handle)
433                        .child(editor),
434                )
435                .when(decision.is_some(), |this| {
436                    if matched_patterns.is_empty() {
437                        this.child(
438                            Label::new("No regex matches, using the default action (confirm).")
439                                .size(LabelSize::Small)
440                                .color(Color::Muted),
441                        )
442                    } else {
443                        this.child(render_matched_patterns(&matched_patterns, cx))
444                    }
445                }),
446        )
447        .into_any_element()
448}
449
450#[derive(Clone, Debug)]
451struct MatchedPattern {
452    pattern: String,
453    rule_type: RuleType,
454    is_overridden: bool,
455}
456
457fn find_matched_patterns(tool_id: &str, input: &str, cx: &App) -> Vec<MatchedPattern> {
458    let settings = AgentSettings::get_global(cx);
459    let rules = match settings.tool_permissions.tools.get(tool_id) {
460        Some(rules) => rules,
461        None => return Vec::new(),
462    };
463
464    let mut matched = Vec::new();
465    let mut has_deny_match = false;
466    let mut has_confirm_match = false;
467
468    for rule in &rules.always_deny {
469        if rule.is_match(input) {
470            has_deny_match = true;
471            matched.push(MatchedPattern {
472                pattern: rule.pattern.clone(),
473                rule_type: RuleType::Deny,
474                is_overridden: false,
475            });
476        }
477    }
478
479    for rule in &rules.always_confirm {
480        if rule.is_match(input) {
481            has_confirm_match = true;
482            matched.push(MatchedPattern {
483                pattern: rule.pattern.clone(),
484                rule_type: RuleType::Confirm,
485                is_overridden: has_deny_match,
486            });
487        }
488    }
489
490    for rule in &rules.always_allow {
491        if rule.is_match(input) {
492            matched.push(MatchedPattern {
493                pattern: rule.pattern.clone(),
494                rule_type: RuleType::Allow,
495                is_overridden: has_deny_match || has_confirm_match,
496            });
497        }
498    }
499
500    matched
501}
502
503fn render_matched_patterns(patterns: &[MatchedPattern], cx: &App) -> AnyElement {
504    v_flex()
505        .gap_1()
506        .children(patterns.iter().map(|pattern| {
507            let (type_label, color) = match pattern.rule_type {
508                RuleType::Deny => ("Always Deny", Color::Error),
509                RuleType::Confirm => ("Always Confirm", Color::Warning),
510                RuleType::Allow => ("Always Allow", Color::Success),
511            };
512
513            let type_color = if pattern.is_overridden {
514                Color::Muted
515            } else {
516                color
517            };
518
519            h_flex()
520                .gap_1()
521                .child(
522                    Label::new(pattern.pattern.clone())
523                        .size(LabelSize::Small)
524                        .color(Color::Muted)
525                        .buffer_font(cx)
526                        .when(pattern.is_overridden, |this| this.strikethrough()),
527                )
528                .child(
529                    Icon::new(IconName::Dash)
530                        .size(IconSize::Small)
531                        .color(Color::Custom(cx.theme().colors().icon_muted.opacity(0.4))),
532                )
533                .child(
534                    Label::new(type_label)
535                        .size(LabelSize::XSmall)
536                        .color(type_color)
537                        .when(pattern.is_overridden, |this| {
538                            this.strikethrough().alpha(0.5)
539                        }),
540                )
541        }))
542        .into_any_element()
543}
544
545fn evaluate_test_input(tool_id: &str, input: &str, cx: &App) -> ToolPermissionDecision {
546    let settings = AgentSettings::get_global(cx);
547
548    // Always pass false for always_allow_tool_actions so we test the actual rules,
549    // not the global override that bypasses all checks.
550    // ShellKind is only used for terminal tool's hardcoded security rules;
551    // for other tools, the check returns None immediately.
552    ToolPermissionDecision::from_input(
553        tool_id,
554        input,
555        &settings.tool_permissions,
556        false,
557        ShellKind::system(),
558    )
559}
560
561fn render_rule_section(
562    tool_id: &'static str,
563    title: &'static str,
564    description: &'static str,
565    rule_type: RuleType,
566    patterns: &[String],
567    cx: &mut Context<SettingsWindow>,
568) -> AnyElement {
569    let section_id = format!("{}-{:?}-section", tool_id, rule_type);
570
571    let user_patterns: Vec<_> = patterns.iter().enumerate().collect();
572
573    v_flex()
574        .id(section_id)
575        .child(Label::new(title))
576        .child(
577            Label::new(description)
578                .size(LabelSize::Small)
579                .color(Color::Muted),
580        )
581        .child(
582            v_flex()
583                .mt_2()
584                .w_full()
585                .gap_1p5()
586                .when(patterns.is_empty(), |this| {
587                    this.child(render_pattern_empty_state(cx))
588                })
589                .when(!user_patterns.is_empty(), |this| {
590                    this.child(v_flex().gap_1p5().children(user_patterns.iter().map(
591                        |(index, pattern)| {
592                            render_user_pattern_row(
593                                tool_id,
594                                rule_type,
595                                *index,
596                                (*pattern).clone(),
597                                cx,
598                            )
599                        },
600                    )))
601                })
602                .child(render_add_pattern_input(tool_id, rule_type, cx)),
603        )
604        .into_any_element()
605}
606
607fn render_pattern_empty_state(cx: &mut Context<SettingsWindow>) -> AnyElement {
608    h_flex()
609        .p_2()
610        .rounded_md()
611        .border_1()
612        .border_dashed()
613        .border_color(cx.theme().colors().border_variant)
614        .child(
615            Label::new("No patterns configured")
616                .size(LabelSize::Small)
617                .color(Color::Disabled),
618        )
619        .into_any_element()
620}
621
622fn render_user_pattern_row(
623    tool_id: &'static str,
624    rule_type: RuleType,
625    index: usize,
626    pattern: String,
627    cx: &mut Context<SettingsWindow>,
628) -> AnyElement {
629    let pattern_for_delete = pattern.clone();
630    let pattern_for_update = pattern.clone();
631    let tool_id_for_delete = tool_id.to_string();
632    let tool_id_for_update = tool_id.to_string();
633    let input_id = format!("{}-{:?}-pattern-{}", tool_id, rule_type, index);
634    let delete_id = format!("{}-{:?}-delete-{}", tool_id, rule_type, index);
635
636    SettingsInputField::new()
637        .with_id(input_id)
638        .with_initial_text(pattern)
639        .tab_index(0)
640        .with_buffer_font()
641        .color(Color::Default)
642        .action_slot(
643            IconButton::new(delete_id, IconName::Trash)
644                .icon_size(IconSize::Small)
645                .icon_color(Color::Muted)
646                .tooltip(Tooltip::text("Delete Pattern"))
647                .on_click(cx.listener(move |_, _, _, cx| {
648                    delete_pattern(&tool_id_for_delete, rule_type, &pattern_for_delete, cx);
649                })),
650        )
651        .on_confirm(move |new_pattern, _window, cx| {
652            if let Some(new_pattern) = new_pattern {
653                let new_pattern = new_pattern.trim().to_string();
654                if !new_pattern.is_empty() && new_pattern != pattern_for_update {
655                    update_pattern(
656                        &tool_id_for_update,
657                        rule_type,
658                        &pattern_for_update,
659                        new_pattern,
660                        cx,
661                    );
662                }
663            }
664        })
665        .into_any_element()
666}
667
668fn render_add_pattern_input(
669    tool_id: &'static str,
670    rule_type: RuleType,
671    _cx: &mut Context<SettingsWindow>,
672) -> AnyElement {
673    let tool_id_owned = tool_id.to_string();
674    let input_id = format!("{}-{:?}-new-pattern", tool_id, rule_type);
675
676    SettingsInputField::new()
677        .with_id(input_id)
678        .with_placeholder("Add regex pattern…")
679        .tab_index(0)
680        .with_buffer_font()
681        .display_clear_button()
682        .display_confirm_button()
683        .on_confirm(move |pattern, _window, cx| {
684            if let Some(pattern) = pattern {
685                if !pattern.trim().is_empty() {
686                    save_pattern(&tool_id_owned, rule_type, pattern.trim().to_string(), cx);
687                }
688            }
689        })
690        .into_any_element()
691}
692
693fn render_default_mode_section(
694    tool_id: &'static str,
695    current_mode: ToolPermissionMode,
696    _cx: &mut Context<SettingsWindow>,
697) -> AnyElement {
698    let mode_label = match current_mode {
699        ToolPermissionMode::Allow => "Allow",
700        ToolPermissionMode::Deny => "Deny",
701        ToolPermissionMode::Confirm => "Confirm",
702    };
703
704    let tool_id_owned = tool_id.to_string();
705
706    h_flex()
707        .justify_between()
708        .child(
709            v_flex().child(Label::new("Default Action")).child(
710                Label::new("Action to take when no patterns match.")
711                    .size(LabelSize::Small)
712                    .color(Color::Muted),
713            ),
714        )
715        .child(
716            PopoverMenu::new(format!("default-mode-{}", tool_id))
717                .trigger(
718                    Button::new(format!("mode-trigger-{}", tool_id), mode_label)
719                        .tab_index(0_isize)
720                        .style(ButtonStyle::Outlined)
721                        .size(ButtonSize::Medium)
722                        .icon(IconName::ChevronDown)
723                        .icon_position(IconPosition::End)
724                        .icon_size(IconSize::Small),
725                )
726                .menu(move |window, cx| {
727                    let tool_id = tool_id_owned.clone();
728                    Some(ContextMenu::build(window, cx, move |menu, _, _| {
729                        let tool_id_confirm = tool_id.clone();
730                        let tool_id_allow = tool_id.clone();
731                        let tool_id_deny = tool_id;
732
733                        menu.entry("Confirm", None, move |_, cx| {
734                            set_default_mode(&tool_id_confirm, ToolPermissionMode::Confirm, cx);
735                        })
736                        .entry("Allow", None, move |_, cx| {
737                            set_default_mode(&tool_id_allow, ToolPermissionMode::Allow, cx);
738                        })
739                        .entry("Deny", None, move |_, cx| {
740                            set_default_mode(&tool_id_deny, ToolPermissionMode::Deny, cx);
741                        })
742                    }))
743                })
744                .anchor(gpui::Corner::TopRight),
745        )
746        .into_any_element()
747}
748
749#[derive(Clone, Copy, PartialEq, Debug)]
750pub(crate) enum RuleType {
751    Allow,
752    Deny,
753    Confirm,
754}
755
756struct ToolRulesView {
757    default_mode: ToolPermissionMode,
758    always_allow: Vec<String>,
759    always_deny: Vec<String>,
760    always_confirm: Vec<String>,
761}
762
763fn get_tool_rules(tool_name: &str, cx: &App) -> ToolRulesView {
764    let settings = AgentSettings::get_global(cx);
765
766    let tool_rules = settings.tool_permissions.tools.get(tool_name);
767
768    match tool_rules {
769        Some(rules) => ToolRulesView {
770            default_mode: rules.default_mode,
771            always_allow: rules
772                .always_allow
773                .iter()
774                .map(|r| r.pattern.clone())
775                .collect(),
776            always_deny: rules
777                .always_deny
778                .iter()
779                .map(|r| r.pattern.clone())
780                .collect(),
781            always_confirm: rules
782                .always_confirm
783                .iter()
784                .map(|r| r.pattern.clone())
785                .collect(),
786        },
787        None => ToolRulesView {
788            default_mode: ToolPermissionMode::Confirm,
789            always_allow: Vec::new(),
790            always_deny: Vec::new(),
791            always_confirm: Vec::new(),
792        },
793    }
794}
795
796fn save_pattern(tool_name: &str, rule_type: RuleType, pattern: String, cx: &mut App) {
797    let tool_name = tool_name.to_string();
798
799    SettingsStore::global(cx).update_settings_file(<dyn fs::Fs>::global(cx), move |settings, _| {
800        let tool_permissions = settings
801            .agent
802            .get_or_insert_default()
803            .tool_permissions
804            .get_or_insert_default();
805        let tool_rules = tool_permissions
806            .tools
807            .entry(Arc::from(tool_name.as_str()))
808            .or_default();
809
810        let rule = settings::ToolRegexRule {
811            pattern,
812            case_sensitive: None,
813        };
814
815        let rules_list = match rule_type {
816            RuleType::Allow => tool_rules.always_allow.get_or_insert_default(),
817            RuleType::Deny => tool_rules.always_deny.get_or_insert_default(),
818            RuleType::Confirm => tool_rules.always_confirm.get_or_insert_default(),
819        };
820
821        if !rules_list.0.iter().any(|r| r.pattern == rule.pattern) {
822            rules_list.0.push(rule);
823        }
824    });
825}
826
827fn update_pattern(
828    tool_name: &str,
829    rule_type: RuleType,
830    old_pattern: &str,
831    new_pattern: String,
832    cx: &mut App,
833) {
834    let tool_name = tool_name.to_string();
835    let old_pattern = old_pattern.to_string();
836
837    SettingsStore::global(cx).update_settings_file(<dyn fs::Fs>::global(cx), move |settings, _| {
838        let tool_permissions = settings
839            .agent
840            .get_or_insert_default()
841            .tool_permissions
842            .get_or_insert_default();
843
844        if let Some(tool_rules) = tool_permissions.tools.get_mut(tool_name.as_str()) {
845            let rules_list = match rule_type {
846                RuleType::Allow => &mut tool_rules.always_allow,
847                RuleType::Deny => &mut tool_rules.always_deny,
848                RuleType::Confirm => &mut tool_rules.always_confirm,
849            };
850
851            if let Some(list) = rules_list {
852                if let Some(rule) = list.0.iter_mut().find(|r| r.pattern == old_pattern) {
853                    rule.pattern = new_pattern;
854                }
855            }
856        }
857    });
858}
859
860fn delete_pattern(tool_name: &str, rule_type: RuleType, pattern: &str, cx: &mut App) {
861    let tool_name = tool_name.to_string();
862    let pattern = pattern.to_string();
863
864    SettingsStore::global(cx).update_settings_file(<dyn fs::Fs>::global(cx), move |settings, _| {
865        let tool_permissions = settings
866            .agent
867            .get_or_insert_default()
868            .tool_permissions
869            .get_or_insert_default();
870
871        if let Some(tool_rules) = tool_permissions.tools.get_mut(tool_name.as_str()) {
872            let rules_list = match rule_type {
873                RuleType::Allow => &mut tool_rules.always_allow,
874                RuleType::Deny => &mut tool_rules.always_deny,
875                RuleType::Confirm => &mut tool_rules.always_confirm,
876            };
877
878            if let Some(list) = rules_list {
879                list.0.retain(|r| r.pattern != pattern);
880            }
881        }
882    });
883}
884
885fn set_default_mode(tool_name: &str, mode: ToolPermissionMode, cx: &mut App) {
886    let tool_name = tool_name.to_string();
887
888    SettingsStore::global(cx).update_settings_file(<dyn fs::Fs>::global(cx), move |settings, _| {
889        let tool_permissions = settings
890            .agent
891            .get_or_insert_default()
892            .tool_permissions
893            .get_or_insert_default();
894        let tool_rules = tool_permissions
895            .tools
896            .entry(Arc::from(tool_name.as_str()))
897            .or_default();
898        tool_rules.default_mode = Some(mode);
899    });
900}
901
902// Macro to generate render functions for each tool
903macro_rules! tool_config_page_fn {
904    ($fn_name:ident, $tool_id:literal) => {
905        pub fn $fn_name(
906            settings_window: &SettingsWindow,
907            scroll_handle: &ScrollHandle,
908            window: &mut Window,
909            cx: &mut Context<SettingsWindow>,
910        ) -> AnyElement {
911            render_tool_config_page($tool_id, settings_window, scroll_handle, window, cx)
912        }
913    };
914}
915
916tool_config_page_fn!(render_terminal_tool_config, "terminal");
917tool_config_page_fn!(render_edit_file_tool_config, "edit_file");
918tool_config_page_fn!(render_delete_path_tool_config, "delete_path");
919tool_config_page_fn!(render_move_path_tool_config, "move_path");
920tool_config_page_fn!(render_create_directory_tool_config, "create_directory");
921tool_config_page_fn!(render_save_file_tool_config, "save_file");
922tool_config_page_fn!(render_fetch_tool_config, "fetch");
923tool_config_page_fn!(render_web_search_tool_config, "web_search");