Cameron Mcloughlinagent: Re-enable sandbox (#61711)12a19dc3 months agoHistory
1976 lines · 71.0 KB · Rust
RawDownload
mod agent_profile;mod user_agents_md; use std::cmp::Ordering::{Equal, Greater, Less};use std::fmt;use std::path::{Component, Path};use std::sync::{Arc, LazyLock}; use anyhow::Context as _;use collections::{HashSet, IndexMap};use fs::Fs;use futures::channel::oneshot;use gpui::{App, Pixels, SharedString, px};use language_model::LanguageModel;use project::DisableAiSettings;use schemars::JsonSchema;use serde::{Deserialize, Serialize};use settings::{    DockPosition, DockSide, LanguageModelParameters, LanguageModelSelection,    NotifyWhenAgentWaiting, PlaySoundWhenAgentDone, RegisterSetting, Settings, SettingsContent,    SettingsStore, SidebarDockPosition, SidebarSide, ThinkingBlockDisplay, ToolPermissionMode,    update_settings_file, update_settings_file_with_completion,};use util::ResultExt as _; pub use crate::agent_profile::*;pub use crate::user_agents_md::{UserAgentsMd, UserAgentsMdState, init as init_user_agents_md}; 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");pub const COMPACTION_PROMPT: &str = include_str!("prompts/compaction_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, Copy, Debug, PartialEq)]pub enum AutoCompactThreshold {    /// Compact once the context window is at least this full, as a fraction in    /// the range `(0.0, 1.0]`.    Percentage(f64),    /// Compact once at least this many tokens have been used.    TokensUsed(u64),    /// Compact once fewer than this many tokens remain in the context window.    TokensRemaining(u64),} impl AutoCompactThreshold {    /// The threshold used when none is configured, or when the configured value    /// is invalid (90% of the context window).    pub const DEFAULT: Self = Self::Percentage(0.9);} impl fmt::Display for AutoCompactThreshold {    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {        match self {            Self::Percentage(percent) => write!(formatter, "{}%", percent * 100.0),            Self::TokensUsed(tokens) => write!(formatter, "{tokens}"),            Self::TokensRemaining(tokens) => write!(formatter, "-{tokens}"),        }    }} #[derive(Clone, Copy, Debug, PartialEq)]pub struct AutoCompactSettings {    pub enabled: bool,    pub threshold: AutoCompactThreshold,} fn parse_auto_compact_threshold(raw: &str) -> anyhow::Result<AutoCompactThreshold> {    let trimmed = raw.trim();    if let Some(percent) = trimmed.strip_suffix('%') {        let value: f64 = percent            .trim_end()            .parse()            .with_context(|| format!("invalid auto_compact threshold percentage {raw:?}"))?;        anyhow::ensure!(            value > 0.0 && value <= 100.0,            "auto_compact threshold percentage must be between 0% and 100%, got {raw:?}"        );        Ok(AutoCompactThreshold::Percentage(value / 100.0))    } else {        let tokens: i64 = trimmed.parse().with_context(|| {            format!(                "invalid auto_compact threshold {raw:?}; \                 expected a percentage like \"90%\" or an integer number of tokens"            )        })?;        match tokens.cmp(&0) {            Greater => Ok(AutoCompactThreshold::TokensUsed(tokens as u64)),            Less => Ok(AutoCompactThreshold::TokensRemaining(tokens.unsigned_abs())),            Equal => {                anyhow::bail!("auto_compact threshold of 0 is not valid")            }        }    }} #[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: Option<Pixels>,    pub default_model: Option<LanguageModelSelection>,    pub subagent_model: Option<LanguageModelSelection>,    pub inline_assistant_model: Option<LanguageModelSelection>,    pub inline_assistant_use_streaming_tools: bool,    pub commit_message_model: Option<LanguageModelSelection>,    pub commit_message_include_project_rules: bool,    pub commit_message_instructions: Option<String>,    pub thread_summary_model: Option<LanguageModelSelection>,    pub compaction_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 auto_compact: AutoCompactSettings,    pub enable_feedback: bool,    pub expand_edit_card: bool,    pub expand_terminal_card: bool,    pub terminal_init_command: Option<String>,    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 sandbox_permissions: SandboxPermissions,} 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<SharedString> {        self.favorite_models            .iter()            .map(|sel| SharedString::from(format!("{}/{}", sel.provider.0, sel.model)))            .collect()    }} pub fn language_model_to_selection(    model: &Arc<dyn LanguageModel>,    override_selection: Option<&LanguageModelSelection>,) -> LanguageModelSelection {    let provider = model.provider_id().0.to_string().into();    let model_name = model.id().0.to_string();    match override_selection {        Some(current) => LanguageModelSelection {            provider,            model: model_name,            enable_thinking: current.enable_thinking && model.supports_thinking(),            effort: current                .effort                .clone()                .filter(|value| {                    model                        .supported_effort_levels()                        .iter()                        .any(|level| level.value.as_ref() == value.as_str())                })                .or_else(|| {                    model                        .default_effort_level()                        .map(|effort| effort.value.to_string())                }),            speed: current.speed.filter(|_| model.supports_fast_mode()),        },        None => LanguageModelSelection {            provider,            model: model_name,            enable_thinking: model.supports_thinking(),            effort: model                .default_effort_level()                .map(|effort| effort.value.to_string()),            speed: None,        },    }} impl AgentSettings {    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,    ) -> oneshot::Receiver<anyhow::Result<()>> {        let merged = PanelLayout::read_from(cx.global::<SettingsStore>().merged_settings());         match layout {            WindowLayout::Agent(None) => {                update_settings_file_with_completion(fs, cx, move |settings, _cx| {                    PanelLayout::AGENT.write_diff_to(&merged, settings);                })            }            WindowLayout::Editor(None) => {                update_settings_file_with_completion(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_with_completion(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())    }} /// Persistent "allow always" sandbox grants for agent-run terminal commands.////// Coverage decisions for these grants are made in/// `agent::sandboxing::ThreadSandboxGrants::covers_with_persistent`, which/// combines them with the in-memory per-thread grants. `write_paths` are/// stored as minimal, lexically-normalized subtrees (see/// [`compile_sandbox_permissions`]).#[derive(Clone, Debug, PartialEq, Eq)]pub struct SandboxPermissions {    /// Allow sandboxed commands to reach any host over the network.    pub allow_all_hosts: bool,    /// Hosts sandboxed commands may always reach, in canonical form (exact    /// hostnames or leading-`*.` subdomain wildcards). Parsed/validated where    /// consumed (`agent::sandboxing`).    pub network_hosts: Vec<String>,    pub allow_fs_write_all: bool,    /// Persistently run agent terminal commands outside the OS sandbox. This is    /// the model-facing "off switch": when set, the sandboxed terminal tool is    /// not exposed and the system prompt omits the sandbox section, so the    /// model uses the plain `terminal` tool (on Windows, WSL sandbox setup is    /// skipped). Distinct from the model-requested `unsandboxed: true` escape    /// approved "once" or "for this thread", which keeps the sandboxed    /// tool/prompt in place — see `agent::sandboxing`.    pub allow_unsandboxed: bool,    /// Directory subtree grants, each paired with the canonical    /// (symlink-resolved) target established when the grant was approved.    pub write_paths: Vec<settings::GrantedWritePath>,    /// Whether sandbox escalation prompts warn about domains or write paths    /// that contain potentially confusable Unicode characters (homoglyphs,    /// invisible characters, or bidirectional overrides). Enabled by default.    pub warn_confusable_unicode: bool,    /// Whether to warn (Windows/WSL only) when a sandbox grant targets a file on    /// a Windows-hosted (DrvFs) filesystem, whose sandbox-integrity guarantees    /// are weaker than a distro-native filesystem. Enabled by default.    pub warn_ntfs_grants: bool,} impl Default for SandboxPermissions {    fn default() -> Self {        Self {            allow_all_hosts: false,            network_hosts: Vec::new(),            allow_fs_write_all: false,            allow_unsandboxed: false,            write_paths: Vec::new(),            // The confusable-Unicode warning is a safety net, so it defaults on.            warn_confusable_unicode: true,            // The weaker-guarantee warning for Windows-hosted grants defaults on.            warn_ntfs_grants: true,        }    }} #[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 {

Lines 1–500 of 1976.