1470 lines · 51.9 KB · Rust
RawDownload
mod agent_profile; use std::path::{Component, Path};use std::sync::{Arc, LazyLock}; use agent_client_protocol::ModelId;use collections::{HashSet, IndexMap};use fs::Fs;use gpui::{App, Pixels, px};use language_model::LanguageModel;use project::DisableAiSettings;use schemars::JsonSchema;use serde::{Deserialize, Serialize};use settings::{    DockPosition, DockSide, LanguageModelParameters, LanguageModelSelection, NewThreadLocation,    NotifyWhenAgentWaiting, PlaySoundWhenAgentDone, RegisterSetting, Settings, SettingsContent,    SettingsStore, SidebarDockPosition, SidebarSide, ThinkingBlockDisplay, ToolPermissionMode,    update_settings_file,}; pub use crate::agent_profile::*; pub const SUMMARIZE_THREAD_PROMPT: &str = include_str!("prompts/summarize_thread_prompt.txt");pub const SUMMARIZE_THREAD_DETAILED_PROMPT: &str =    include_str!("prompts/summarize_thread_detailed_prompt.txt"); #[derive(Debug, Clone, Default, PartialEq, Eq)]pub struct PanelLayout {    pub(crate) agent_dock: Option<DockPosition>,    pub(crate) project_panel_dock: Option<DockSide>,    pub(crate) outline_panel_dock: Option<DockSide>,    pub(crate) collaboration_panel_dock: Option<DockPosition>,    pub(crate) git_panel_dock: Option<DockPosition>,} impl PanelLayout {    const AGENT: Self = Self {        agent_dock: Some(DockPosition::Left),        project_panel_dock: Some(DockSide::Right),        outline_panel_dock: Some(DockSide::Right),        collaboration_panel_dock: Some(DockPosition::Right),        git_panel_dock: Some(DockPosition::Right),    };     const EDITOR: Self = Self {        agent_dock: Some(DockPosition::Right),        project_panel_dock: Some(DockSide::Left),        outline_panel_dock: Some(DockSide::Left),        collaboration_panel_dock: Some(DockPosition::Left),        git_panel_dock: Some(DockPosition::Left),    };     pub fn is_agent_layout(&self) -> bool {        *self == Self::AGENT    }     pub fn is_editor_layout(&self) -> bool {        *self == Self::EDITOR    }     fn read_from(content: &SettingsContent) -> Self {        Self {            agent_dock: content.agent.as_ref().and_then(|a| a.dock),            project_panel_dock: content.project_panel.as_ref().and_then(|p| p.dock),            outline_panel_dock: content.outline_panel.as_ref().and_then(|p| p.dock),            collaboration_panel_dock: content.collaboration_panel.as_ref().and_then(|p| p.dock),            git_panel_dock: content.git_panel.as_ref().and_then(|p| p.dock),        }    }     fn write_to(&self, settings: &mut SettingsContent) {        settings.agent.get_or_insert_default().dock = self.agent_dock;        settings.project_panel.get_or_insert_default().dock = self.project_panel_dock;        settings.outline_panel.get_or_insert_default().dock = self.outline_panel_dock;        settings.collaboration_panel.get_or_insert_default().dock = self.collaboration_panel_dock;        settings.git_panel.get_or_insert_default().dock = self.git_panel_dock;    }     fn write_diff_to(&self, current_merged: &PanelLayout, settings: &mut SettingsContent) {        if self.agent_dock != current_merged.agent_dock {            settings.agent.get_or_insert_default().dock = self.agent_dock;        }        if self.project_panel_dock != current_merged.project_panel_dock {            settings.project_panel.get_or_insert_default().dock = self.project_panel_dock;        }        if self.outline_panel_dock != current_merged.outline_panel_dock {            settings.outline_panel.get_or_insert_default().dock = self.outline_panel_dock;        }        if self.collaboration_panel_dock != current_merged.collaboration_panel_dock {            settings.collaboration_panel.get_or_insert_default().dock =                self.collaboration_panel_dock;        }        if self.git_panel_dock != current_merged.git_panel_dock {            settings.git_panel.get_or_insert_default().dock = self.git_panel_dock;        }    }     fn backfill_to(&self, user_layout: &PanelLayout, settings: &mut SettingsContent) {        if user_layout.agent_dock.is_none() {            settings.agent.get_or_insert_default().dock = self.agent_dock;        }        if user_layout.project_panel_dock.is_none() {            settings.project_panel.get_or_insert_default().dock = self.project_panel_dock;        }        if user_layout.outline_panel_dock.is_none() {            settings.outline_panel.get_or_insert_default().dock = self.outline_panel_dock;        }        if user_layout.collaboration_panel_dock.is_none() {            settings.collaboration_panel.get_or_insert_default().dock =                self.collaboration_panel_dock;        }        if user_layout.git_panel_dock.is_none() {            settings.git_panel.get_or_insert_default().dock = self.git_panel_dock;        }    }} #[derive(Debug, Clone, PartialEq, Eq)]pub enum WindowLayout {    Editor(Option<PanelLayout>),    Agent(Option<PanelLayout>),    Custom(PanelLayout),} impl WindowLayout {    pub fn agent() -> Self {        Self::Agent(None)    }     pub fn editor() -> Self {        Self::Editor(None)    }} #[derive(Clone, Debug, RegisterSetting)]pub struct AgentSettings {    pub enabled: bool,    pub button: bool,    pub dock: DockPosition,    pub flexible: bool,    pub sidebar_side: SidebarDockPosition,    pub default_width: Pixels,    pub default_height: Pixels,    pub max_content_width: Pixels,    pub default_model: Option<LanguageModelSelection>,    pub inline_assistant_model: Option<LanguageModelSelection>,    pub inline_assistant_use_streaming_tools: bool,    pub commit_message_model: Option<LanguageModelSelection>,    pub thread_summary_model: Option<LanguageModelSelection>,    pub inline_alternatives: Vec<LanguageModelSelection>,    pub favorite_models: Vec<LanguageModelSelection>,    pub default_profile: AgentProfileId,    pub profiles: IndexMap<AgentProfileId, AgentProfileSettings>,     pub notify_when_agent_waiting: NotifyWhenAgentWaiting,    pub play_sound_when_agent_done: PlaySoundWhenAgentDone,    pub single_file_review: bool,    pub model_parameters: Vec<LanguageModelParameters>,    pub enable_feedback: bool,    pub expand_edit_card: bool,    pub expand_terminal_card: bool,    pub thinking_display: ThinkingBlockDisplay,    pub cancel_generation_on_terminal_stop: bool,    pub use_modifier_to_send: bool,    pub message_editor_min_lines: usize,    pub show_turn_stats: bool,    pub show_merge_conflict_indicator: bool,    pub tool_permissions: ToolPermissions,    pub new_thread_location: NewThreadLocation,} impl AgentSettings {    pub fn enabled(&self, cx: &App) -> bool {        self.enabled && !DisableAiSettings::get_global(cx).disable_ai    }     pub fn temperature_for_model(model: &Arc<dyn LanguageModel>, cx: &App) -> Option<f32> {        let settings = Self::get_global(cx);        for setting in settings.model_parameters.iter().rev() {            if let Some(provider) = &setting.provider                && provider.0 != model.provider_id().0            {                continue;            }            if let Some(setting_model) = &setting.model                && *setting_model != model.id().0            {                continue;            }            return setting.temperature;        }        return None;    }     pub fn sidebar_side(&self) -> SidebarSide {        match self.sidebar_side {            SidebarDockPosition::Left => SidebarSide::Left,            SidebarDockPosition::Right => SidebarSide::Right,        }    }     pub fn set_message_editor_max_lines(&self) -> usize {        self.message_editor_min_lines * 2    }     pub fn favorite_model_ids(&self) -> HashSet<ModelId> {        self.favorite_models            .iter()            .map(|sel| ModelId::new(format!("{}/{}", sel.provider.0, sel.model)))            .collect()    }     pub fn get_layout(cx: &App) -> WindowLayout {        let store = cx.global::<SettingsStore>();        let merged = store.merged_settings();        let user_layout = store            .raw_user_settings()            .map(|u| PanelLayout::read_from(u.content.as_ref()))            .unwrap_or_default();        let merged_layout = PanelLayout::read_from(merged);         if merged_layout.is_agent_layout() {            return WindowLayout::Agent(Some(user_layout));        }         if merged_layout.is_editor_layout() {            return WindowLayout::Editor(Some(user_layout));        }         WindowLayout::Custom(user_layout)    }     pub fn backfill_editor_layout(fs: Arc<dyn Fs>, cx: &App) {        let user_layout = cx            .global::<SettingsStore>()            .raw_user_settings()            .map(|u| PanelLayout::read_from(u.content.as_ref()))            .unwrap_or_default();         update_settings_file(fs, cx, move |settings, _cx| {            PanelLayout::EDITOR.backfill_to(&user_layout, settings);        });    }     pub fn set_layout(layout: WindowLayout, fs: Arc<dyn Fs>, cx: &App) {        let merged = PanelLayout::read_from(cx.global::<SettingsStore>().merged_settings());         match layout {            WindowLayout::Agent(None) => {                update_settings_file(fs, cx, move |settings, _cx| {                    PanelLayout::AGENT.write_diff_to(&merged, settings);                });            }            WindowLayout::Editor(None) => {                update_settings_file(fs, cx, move |settings, _cx| {                    PanelLayout::EDITOR.write_diff_to(&merged, settings);                });            }            WindowLayout::Agent(Some(saved))            | WindowLayout::Editor(Some(saved))            | WindowLayout::Custom(saved) => {                update_settings_file(fs, cx, move |settings, _cx| {                    saved.write_to(settings);                });            }        }    }} #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)]pub struct AgentProfileId(pub Arc<str>); impl AgentProfileId {    pub fn as_str(&self) -> &str {        &self.0    }} impl std::fmt::Display for AgentProfileId {    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {        write!(f, "{}", self.0)    }} impl Default for AgentProfileId {    fn default() -> Self {        Self("write".into())    }} #[derive(Clone, Debug, Default)]pub struct ToolPermissions {    /// Global default permission when no tool-specific rules or patterns match.    pub default: ToolPermissionMode,    pub tools: collections::HashMap<Arc<str>, ToolRules>,} impl ToolPermissions {    /// Returns all invalid regex patterns across all tools.    pub fn invalid_patterns(&self) -> Vec<&InvalidRegexPattern> {        self.tools            .values()            .flat_map(|rules| rules.invalid_patterns.iter())            .collect()    }     /// Returns true if any tool has invalid regex patterns.    pub fn has_invalid_patterns(&self) -> bool {        self.tools            .values()            .any(|rules| !rules.invalid_patterns.is_empty())    }} /// Represents a regex pattern that failed to compile.#[derive(Clone, Debug)]pub struct InvalidRegexPattern {    /// The pattern string that failed to compile.    pub pattern: String,    /// Which rule list this pattern was in (e.g., "always_deny", "always_allow", "always_confirm").    pub rule_type: String,    /// The error message from the regex compiler.    pub error: String,} #[derive(Clone, Debug, Default)]pub struct ToolRules {    pub default: Option<ToolPermissionMode>,    pub always_allow: Vec<CompiledRegex>,    pub always_deny: Vec<CompiledRegex>,    pub always_confirm: Vec<CompiledRegex>,    /// Patterns that failed to compile. If non-empty, tool calls should be blocked.    pub invalid_patterns: Vec<InvalidRegexPattern>,} #[derive(Clone)]pub struct CompiledRegex {    pub pattern: String,    pub case_sensitive: bool,    pub regex: regex::Regex,} impl std::fmt::Debug for CompiledRegex {    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {        f.debug_struct("CompiledRegex")            .field("pattern", &self.pattern)            .field("case_sensitive", &self.case_sensitive)            .finish()    }} impl CompiledRegex {    pub fn new(pattern: &str, case_sensitive: bool) -> Option<Self> {        Self::try_new(pattern, case_sensitive).ok()    }     pub fn try_new(pattern: &str, case_sensitive: bool) -> Result<Self, regex::Error> {        let regex = regex::RegexBuilder::new(pattern)            .case_insensitive(!case_sensitive)            .build()?;        Ok(Self {            pattern: pattern.to_string(),            case_sensitive,            regex,        })    }     pub fn is_match(&self, input: &str) -> bool {        self.regex.is_match(input)    }} pub const HARDCODED_SECURITY_DENIAL_MESSAGE: &str = "Blocked by built-in security rule. This operation is considered too \     harmful to be allowed, and cannot be overridden by settings."; /// Security rules that are always enforced and cannot be overridden by any setting./// These protect against catastrophic operations like wiping filesystems.pub struct HardcodedSecurityRules {    pub terminal_deny: Vec<CompiledRegex>,} pub static HARDCODED_SECURITY_RULES: LazyLock<HardcodedSecurityRules> = LazyLock::new(|| {    const FLAGS: &str = r"(--[a-zA-Z0-9][-a-zA-Z0-9_]*(=[^\s]*)?\s+|-[a-zA-Z]+\s+)*";    const TRAILING_FLAGS: &str = r"(\s+--[a-zA-Z0-9][-a-zA-Z0-9_]*(=[^\s]*)?|\s+-[a-zA-Z]+)*\s*";     HardcodedSecurityRules {        terminal_deny: vec![            // Recursive deletion of root - "rm -rf /", "rm -rf /*"            CompiledRegex::new(                &format!(r"\brm\s+{FLAGS}(--\s+)?/\*?{TRAILING_FLAGS}$"),                false,            )            .expect("hardcoded regex should compile"),            // Recursive deletion of home via tilde - "rm -rf ~", "rm -rf ~/"            CompiledRegex::new(                &format!(r"\brm\s+{FLAGS}(--\s+)?~/?\*?{TRAILING_FLAGS}$"),                false,            )            .expect("hardcoded regex should compile"),            // Recursive deletion of home via env var - "rm -rf $HOME", "rm -rf ${HOME}"            CompiledRegex::new(                &format!(r"\brm\s+{FLAGS}(--\s+)?(\$HOME|\$\{{HOME\}})/?(\*)?{TRAILING_FLAGS}$"),                false,            )            .expect("hardcoded regex should compile"),            // Recursive deletion of current directory - "rm -rf .", "rm -rf ./"            CompiledRegex::new(                &format!(r"\brm\s+{FLAGS}(--\s+)?\./?\*?{TRAILING_FLAGS}$"),                false,            )            .expect("hardcoded regex should compile"),            // Recursive deletion of parent directory - "rm -rf ..", "rm -rf ../"            CompiledRegex::new(                &format!(r"\brm\s+{FLAGS}(--\s+)?\.\./?\*?{TRAILING_FLAGS}$"),                false,            )            .expect("hardcoded regex should compile"),        ],    }}); /// Checks if input matches any hardcoded security rules that cannot be bypassed./// Returns the denial reason string if blocked, None otherwise.////// `terminal_tool_name` should be the tool name used for the terminal tool/// (e.g. `"terminal"`). `extracted_commands` can optionally provide parsed/// sub-commands for chained command checking; callers with access to a shell/// parser should extract sub-commands and pass them here.pub fn check_hardcoded_security_rules(    tool_name: &str,    terminal_tool_name: &str,    input: &str,    extracted_commands: Option<&[String]>,) -> Option<String> {    if tool_name != terminal_tool_name {        return None;    }     let rules = &*HARDCODED_SECURITY_RULES;    let terminal_patterns = &rules.terminal_deny;     if matches_hardcoded_patterns(input, terminal_patterns) {        return Some(HARDCODED_SECURITY_DENIAL_MESSAGE.into());    }     if let Some(commands) = extracted_commands {        for command in commands {            if matches_hardcoded_patterns(command, terminal_patterns) {                return Some(HARDCODED_SECURITY_DENIAL_MESSAGE.into());            }        }    }     None} fn matches_hardcoded_patterns(command: &str, patterns: &[CompiledRegex]) -> bool {    for pattern in patterns {        if pattern.is_match(command) {            return true;        }    }     for expanded in expand_rm_to_single_path_commands(command) {        for pattern in patterns {            if pattern.is_match(&expanded) {                return true;            }        }    }     false} fn expand_rm_to_single_path_commands(command: &str) -> Vec<String> {    let trimmed = command.trim();     let first_token = trimmed.split_whitespace().next();    if !first_token.is_some_and(|t| t.eq_ignore_ascii_case("rm")) {        return vec![];    }     let parts: Vec<&str> = trimmed.split_whitespace().collect();    let mut flags = Vec::new();    let mut paths = Vec::new();    let mut past_double_dash = false;     for part in parts.iter().skip(1) {        if !past_double_dash && *part == "--" {            past_double_dash = true;            flags.push(*part);            continue;        }        if !past_double_dash && part.starts_with('-') {            flags.push(*part);        } else {            paths.push(*part);        }    } 

Lines 1–500 of 1470.