1018 lines · 34.6 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 gpui::{App, Pixels, px};use language_model::LanguageModel;use project::DisableAiSettings;use schemars::JsonSchema;use serde::{Deserialize, Serialize};use settings::{    DefaultAgentView, DockPosition, LanguageModelParameters, LanguageModelSelection,    NotifyWhenAgentWaiting, RegisterSetting, Settings, ToolPermissionMode,}; 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(Clone, Debug, RegisterSetting)]pub struct AgentSettings {    pub enabled: bool,    pub button: bool,    pub dock: DockPosition,    pub default_width: Pixels,    pub default_height: 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 default_view: DefaultAgentView,    pub profiles: IndexMap<AgentProfileId, AgentProfileSettings>,     pub notify_when_agent_waiting: NotifyWhenAgentWaiting,    pub play_sound_when_agent_done: bool,    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 cancel_generation_on_terminal_stop: bool,    pub use_modifier_to_send: bool,    pub message_editor_min_lines: usize,    pub show_turn_stats: bool,    pub tool_permissions: ToolPermissions,} 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 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()    }} #[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);        }    }     let flags_str = if flags.is_empty() {        String::new()    } else {        format!("{} ", flags.join(" "))    };     let mut results = Vec::new();    for path in &paths {        if path.starts_with('$') {            let home_prefix = if path.starts_with("${HOME}") {                Some("${HOME}")            } else if path.starts_with("$HOME") {                Some("$HOME")            } else {                None            };             if let Some(prefix) = home_prefix {                let suffix = &path[prefix.len()..];                if suffix.is_empty() {                    results.push(format!("rm {flags_str}{path}"));                } else if suffix.starts_with('/') {                    let normalized_suffix = normalize_path(suffix);                    let reconstructed = if normalized_suffix == "/" {                        prefix.to_string()                    } else {                        format!("{prefix}{normalized_suffix}")                    };                    results.push(format!("rm {flags_str}{reconstructed}"));                } else {                    results.push(format!("rm {flags_str}{path}"));                }            } else {                results.push(format!("rm {flags_str}{path}"));            }            continue;        }         let mut normalized = normalize_path(path);        if normalized.is_empty() && !Path::new(path).has_root() {            normalized = ".".to_string();        }         results.push(format!("rm {flags_str}{normalized}"));    }     results} pub fn normalize_path(raw: &str) -> String {    let is_absolute = Path::new(raw).has_root();    let mut components: Vec<&str> = Vec::new();    for component in Path::new(raw).components() {        match component {            Component::CurDir => {}            Component::ParentDir => {                if components.last() == Some(&"..") {                    components.push("..");                } else if !components.is_empty() {                    components.pop();                } else if !is_absolute {                    components.push("..");                }            }            Component::Normal(segment) => {                if let Some(s) = segment.to_str() {                    components.push(s);                }            }            Component::RootDir | Component::Prefix(_) => {}        }    }    let joined = components.join("/");    if is_absolute {        format!("/{joined}")    } else {        joined    }} impl Settings for AgentSettings {    fn from_settings(content: &settings::SettingsContent) -> Self {        let agent = content.agent.clone().unwrap();        Self {            enabled: agent.enabled.unwrap(),            button: agent.button.unwrap(),            dock: agent.dock.unwrap(),            default_width: px(agent.default_width.unwrap()),            default_height: px(agent.default_height.unwrap()),            default_model: Some(agent.default_model.unwrap()),            inline_assistant_model: agent.inline_assistant_model,            inline_assistant_use_streaming_tools: agent                .inline_assistant_use_streaming_tools                .unwrap_or(true),            commit_message_model: agent.commit_message_model,            thread_summary_model: agent.thread_summary_model,            inline_alternatives: agent.inline_alternatives.unwrap_or_default(),            favorite_models: agent.favorite_models,            default_profile: AgentProfileId(agent.default_profile.unwrap()),            default_view: agent.default_view.unwrap(),            profiles: agent                .profiles                .unwrap()                .into_iter()                .map(|(key, val)| (AgentProfileId(key), val.into()))                .collect(),             notify_when_agent_waiting: agent.notify_when_agent_waiting.unwrap(),            play_sound_when_agent_done: agent.play_sound_when_agent_done.unwrap(),            single_file_review: agent.single_file_review.unwrap(),            model_parameters: agent.model_parameters,            enable_feedback: agent.enable_feedback.unwrap(),            expand_edit_card: agent.expand_edit_card.unwrap(),            expand_terminal_card: agent.expand_terminal_card.unwrap(),            cancel_generation_on_terminal_stop: agent.cancel_generation_on_terminal_stop.unwrap(),            use_modifier_to_send: agent.use_modifier_to_send.unwrap(),            message_editor_min_lines: agent.message_editor_min_lines.unwrap(),            show_turn_stats: agent.show_turn_stats.unwrap(),            tool_permissions: compile_tool_permissions(agent.tool_permissions),        }    }} fn compile_tool_permissions(content: Option<settings::ToolPermissionsContent>) -> ToolPermissions {    let Some(content) = content else {        return ToolPermissions::default();    };     let tools = content        .tools        .into_iter()        .map(|(tool_name, rules_content)| {            let mut invalid_patterns = Vec::new();             let (always_allow, allow_errors) = compile_regex_rules(                rules_content.always_allow.map(|v| v.0).unwrap_or_default(),                "always_allow",            );            invalid_patterns.extend(allow_errors);             let (always_deny, deny_errors) = compile_regex_rules(                rules_content.always_deny.map(|v| v.0).unwrap_or_default(),                "always_deny",            );            invalid_patterns.extend(deny_errors);             let (always_confirm, confirm_errors) = compile_regex_rules(                rules_content                    .always_confirm                    .map(|v| v.0)                    .unwrap_or_default(),                "always_confirm",            );            invalid_patterns.extend(confirm_errors);             // Log invalid patterns for debugging. Users will see an error when they            // attempt to use a tool with invalid patterns in their settings.            for invalid in &invalid_patterns {                log::error!(                    "Invalid regex pattern in tool_permissions for '{}' tool ({}): '{}' - {}",                    tool_name,                    invalid.rule_type,                    invalid.pattern,                    invalid.error,                );            }             let rules = ToolRules {                // Preserve tool-specific default; None means fall back to global default at decision time                default: rules_content.default,                always_allow,                always_deny,                always_confirm,                invalid_patterns,            };            (tool_name, rules)        })        .collect(); 

Lines 1–500 of 1018.