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. Load more
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); } }