1244 lines
46 KiB
Rust
1244 lines
46 KiB
Rust
use std::collections::{HashMap, HashSet};
|
|
use std::path::PathBuf;
|
|
|
|
use anyhow::Result;
|
|
use galaxy_completer::parsers::simple::{command_without_leading_env_vars, decompose_command};
|
|
use galaxy_core::execution_mode::AppExecutionMode;
|
|
use galaxy_core::features::FeatureFlag;
|
|
use galaxy_core::settings::Setting;
|
|
use galaxy_core::user_preferences::GetUserPreferences;
|
|
use galaxy_util::path::EscapeChar;
|
|
use galaxyui::{AppContext, Entity, EntityId, ModelContext, SingletonEntity};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use super::BlocklistAIHistoryModel;
|
|
use crate::ai::agent::conversation::AIConversationId;
|
|
use crate::ai::execution_profiles::profiles::{AIExecutionProfilesModel, ClientProfileId};
|
|
use crate::ai::execution_profiles::{
|
|
AIExecutionProfile, ActionPermission, AskUserQuestionPermission, WriteToPtyPermission,
|
|
};
|
|
use crate::ai::mcp::mcp_provider_from_file_path;
|
|
#[cfg(not(target_family = "wasm"))]
|
|
use crate::ai::mcp::TemplatableMCPServerManager;
|
|
use crate::report_if_error;
|
|
use crate::settings::{
|
|
AISettings, AgentModeCodingPermissionsType, AgentModeCommandExecutionPredicate,
|
|
};
|
|
use crate::workspaces::user_workspaces::UserWorkspaces;
|
|
use crate::workspaces::workspace::AiAutonomySettings;
|
|
|
|
/// Whether or not a command can be auto-executed, along with a detailed reason.
|
|
#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
|
|
pub enum CommandExecutionPermission {
|
|
Allowed(CommandExecutionPermissionAllowedReason),
|
|
Denied(CommandExecutionPermissionDeniedReason),
|
|
}
|
|
|
|
/// Why a command can be auto-executed.
|
|
#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
|
|
pub enum CommandExecutionPermissionAllowedReason {
|
|
Dispatched,
|
|
ExplicitlyAllowlisted,
|
|
IsReadOnlyAndSettingEnabled,
|
|
AgentDecided,
|
|
AlwaysAllowed,
|
|
RunToCompletion,
|
|
}
|
|
|
|
/// Why a command can't be auto-executed.
|
|
#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
|
|
pub enum CommandExecutionPermissionDeniedReason {
|
|
AutonomyForceDisabled,
|
|
AlwaysAskEnabled,
|
|
ExplicitlyDenylisted,
|
|
ContainsRedirection,
|
|
Inconclusive,
|
|
AgentDecided,
|
|
}
|
|
|
|
impl CommandExecutionPermission {
|
|
pub fn is_allowed(&self) -> bool {
|
|
matches!(self, Self::Allowed(..))
|
|
}
|
|
}
|
|
|
|
/// Whether or not a file can be auto-read, along with a detailed reason.
|
|
#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
|
|
pub enum FileReadPermission {
|
|
Allowed(FileReadPermissionAllowedReason),
|
|
Denied(FileReadPermissionDeniedReason),
|
|
}
|
|
|
|
/// Why a file can be auto-read.
|
|
#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
|
|
pub enum FileReadPermissionAllowedReason {
|
|
Dispatched,
|
|
AlreadyReadInConvo,
|
|
ExplicitlyAllowlisted,
|
|
AutoreadSettingEnabled,
|
|
AgentDecided,
|
|
RunToCompletion,
|
|
}
|
|
|
|
/// Why a file can't be auto-read.
|
|
#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
|
|
pub enum FileReadPermissionDeniedReason {
|
|
AutonomyForceDisabled,
|
|
AlwaysAskEnabled,
|
|
Inconclusive,
|
|
AgentDecided,
|
|
}
|
|
|
|
impl FileReadPermission {
|
|
pub fn is_allowed(&self) -> bool {
|
|
matches!(self, Self::Allowed(..))
|
|
}
|
|
}
|
|
|
|
/// Whether or not a file can be auto-written, along with a detailed reason.
|
|
#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
|
|
pub enum FileWritePermission {
|
|
Allowed(FileWritePermissionAllowedReason),
|
|
Denied(FileWritePermissionDeniedReason),
|
|
}
|
|
|
|
impl FileWritePermission {
|
|
pub fn is_allowed(&self) -> bool {
|
|
matches!(self, Self::Allowed(..))
|
|
}
|
|
}
|
|
|
|
/// Why a file can be written automatically.
|
|
#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
|
|
pub enum FileWritePermissionAllowedReason {
|
|
Dispatched,
|
|
AgentDecided,
|
|
AutowriteSettingEnabled,
|
|
RunToCompletion,
|
|
}
|
|
|
|
/// Why a file can't be written automatically.
|
|
#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
|
|
pub enum FileWritePermissionDeniedReason {
|
|
AutonomyForceDisabled,
|
|
AlwaysAskEnabled,
|
|
Inconclusive,
|
|
AgentDecided,
|
|
/// The path is a system-protected file (e.g. an MCP config) that must never
|
|
/// be auto-written regardless of user autonomy settings.
|
|
ProtectedPath,
|
|
}
|
|
|
|
/// Describes permissions that Agent Mode has, backed by [`AISettings`].
|
|
pub struct BlocklistAIPermissions {
|
|
/// A set of one-off files that the user has allowed Agent Mode
|
|
/// to read for the duration of a given conversation.
|
|
///
|
|
/// TODO: remove this once AM doesn't re-request access to the same file in a given convo.
|
|
temporary_file_permissions: HashMap<AIConversationId, HashSet<PathBuf>>,
|
|
}
|
|
|
|
impl BlocklistAIPermissions {
|
|
pub fn new(ctx: &mut ModelContext<Self>) -> Self {
|
|
// Migrate the old `AgentModeAutoReadFiles` setting to the new [`AgentModeCodingPermissionsType`].
|
|
if let Some(can_read_files) = ctx
|
|
.private_user_preferences()
|
|
.read_value("AgentModeAutoReadFiles")
|
|
.ok()
|
|
.flatten()
|
|
.and_then(|s| s.parse().ok())
|
|
{
|
|
if let Err(e) = ctx
|
|
.private_user_preferences()
|
|
.remove_value("AgentModeAutoReadFiles")
|
|
{
|
|
log::error!("Failed to remove old AgentModeAutoReadFiles user pref: {e}");
|
|
}
|
|
if can_read_files {
|
|
report_if_error!(AISettings::handle(ctx).update(ctx, |settings, ctx| {
|
|
settings
|
|
.agent_mode_coding_permissions
|
|
.set_value(AgentModeCodingPermissionsType::AlwaysAllowReading, ctx)
|
|
}));
|
|
}
|
|
}
|
|
|
|
Self {
|
|
temporary_file_permissions: Default::default(),
|
|
}
|
|
}
|
|
|
|
/// Returns the active permissions profile, accounting for any enterprise overrides.
|
|
pub fn permissions_profile_for_id(
|
|
&self,
|
|
ctx: &AppContext,
|
|
profile_id: ClientProfileId,
|
|
) -> AIExecutionProfile {
|
|
let profiles_model = AIExecutionProfilesModel::as_ref(ctx);
|
|
let profile = profiles_model
|
|
.get_profile_by_id(profile_id, ctx)
|
|
.unwrap_or_else(|| profiles_model.default_profile(ctx));
|
|
let profile_data = profile.data();
|
|
|
|
AIExecutionProfile {
|
|
// Some fields may have an enterprise override.
|
|
apply_code_diffs: self.get_apply_code_diffs_setting_for_profile(ctx, profile_id),
|
|
read_files: self.get_read_files_setting_for_profile(ctx, profile_id),
|
|
execute_commands: self.get_execute_commands_setting_for_profile(ctx, profile_id),
|
|
mcp_permissions: self.get_mcp_permissions_setting_for_profile(ctx, profile_id),
|
|
write_to_pty: self.get_write_to_pty_setting_for_profile(ctx, profile_id),
|
|
command_allowlist: self.get_execute_commands_allowlist_for_profile(ctx, profile_id),
|
|
command_denylist: self.get_execute_commands_denylist_for_profile(ctx, profile_id),
|
|
directory_allowlist: self.get_read_files_allowlist_for_profile(ctx, profile_id),
|
|
mcp_allowlist: self.get_mcp_allowlist_for_profile(ctx, profile_id),
|
|
mcp_denylist: self.get_mcp_denylist_for_profile(ctx, profile_id),
|
|
computer_use: self.get_computer_use_setting_for_profile(ctx, profile_id),
|
|
ask_user_question: self.get_ask_user_question_setting_for_profile(ctx, profile_id),
|
|
run_agents: self.get_run_agents_setting_for_profile(ctx, profile_id),
|
|
|
|
// Some fields are read directly from the profile.
|
|
name: profile_data.name.clone(),
|
|
is_default_profile: profile_data.is_default_profile,
|
|
base_model: profile_data.base_model.clone(),
|
|
coding_model: profile_data.coding_model.clone(),
|
|
cli_agent_model: profile_data.cli_agent_model.clone(),
|
|
computer_use_model: profile_data.computer_use_model.clone(),
|
|
context_window_limit: profile_data.context_window_limit,
|
|
autosync_plans_to_warp_drive: profile_data.autosync_plans_to_warp_drive,
|
|
web_search_enabled: profile_data.web_search_enabled,
|
|
}
|
|
}
|
|
|
|
pub fn active_permissions_profile(
|
|
&self,
|
|
ctx: &AppContext,
|
|
terminal_view_id: Option<EntityId>,
|
|
) -> AIExecutionProfile {
|
|
let active_profile =
|
|
AIExecutionProfilesModel::as_ref(ctx).active_profile(terminal_view_id, ctx);
|
|
self.permissions_profile_for_id(ctx, *active_profile.id())
|
|
}
|
|
|
|
/// Returns the applicable workspace autonomy settings based on execution mode.
|
|
/// In sandboxed mode, returns settings derived from the sandboxed agent config.
|
|
/// In unsandboxed mode, returns the standard AI autonomy settings.
|
|
fn workspace_autonomy_settings(ctx: &AppContext) -> AiAutonomySettings {
|
|
if AppExecutionMode::as_ref(ctx).is_sandboxed() {
|
|
let sandboxed = UserWorkspaces::as_ref(ctx).sandboxed_agent_settings();
|
|
AiAutonomySettings {
|
|
execute_commands_denylist: sandboxed.and_then(|s| s.execute_commands_denylist),
|
|
..Default::default()
|
|
}
|
|
} else {
|
|
UserWorkspaces::as_ref(ctx).ai_autonomy_settings()
|
|
}
|
|
}
|
|
|
|
pub fn get_apply_code_diffs_setting_for_profile(
|
|
&self,
|
|
ctx: &AppContext,
|
|
profile_id: ClientProfileId,
|
|
) -> ActionPermission {
|
|
let autonomy_settings = Self::workspace_autonomy_settings(ctx);
|
|
let apply_code_diffs_workspace_setting = autonomy_settings.apply_code_diffs_setting;
|
|
|
|
apply_code_diffs_workspace_setting.unwrap_or_else(|| {
|
|
let profiles_model = AIExecutionProfilesModel::as_ref(ctx);
|
|
profiles_model
|
|
.get_profile_by_id(profile_id, ctx)
|
|
.unwrap_or_else(|| profiles_model.default_profile(ctx))
|
|
.data()
|
|
.apply_code_diffs
|
|
})
|
|
}
|
|
|
|
/// Returns what the current setting is for applying code diffs,
|
|
/// based on the workspace setting and the active profile.
|
|
pub fn get_apply_code_diffs_setting(
|
|
&self,
|
|
ctx: &AppContext,
|
|
terminal_view_id: Option<EntityId>,
|
|
) -> ActionPermission {
|
|
let active_profile =
|
|
AIExecutionProfilesModel::as_ref(ctx).active_profile(terminal_view_id, ctx);
|
|
|
|
self.get_apply_code_diffs_setting_for_profile(ctx, *active_profile.id())
|
|
}
|
|
|
|
pub fn get_read_files_setting_for_profile(
|
|
&self,
|
|
ctx: &AppContext,
|
|
profile_id: ClientProfileId,
|
|
) -> ActionPermission {
|
|
let autonomy_settings = Self::workspace_autonomy_settings(ctx);
|
|
let read_files_workspace_setting = autonomy_settings.read_files_setting;
|
|
|
|
read_files_workspace_setting.unwrap_or_else(|| {
|
|
let profiles_model = AIExecutionProfilesModel::as_ref(ctx);
|
|
profiles_model
|
|
.get_profile_by_id(profile_id, ctx)
|
|
.unwrap_or_else(|| profiles_model.default_profile(ctx))
|
|
.data()
|
|
.read_files
|
|
})
|
|
}
|
|
|
|
/// Returns what the current setting is for reading files,
|
|
/// based on the workspace setting and the active profile.
|
|
pub fn get_read_files_setting(
|
|
&self,
|
|
ctx: &AppContext,
|
|
terminal_view_id: Option<EntityId>,
|
|
) -> ActionPermission {
|
|
let active_profile =
|
|
AIExecutionProfilesModel::as_ref(ctx).active_profile(terminal_view_id, ctx);
|
|
self.get_read_files_setting_for_profile(ctx, *active_profile.id())
|
|
}
|
|
|
|
pub fn get_read_files_allowlist_for_profile(
|
|
&self,
|
|
ctx: &AppContext,
|
|
profile_id: ClientProfileId,
|
|
) -> Vec<PathBuf> {
|
|
let autonomy_settings = Self::workspace_autonomy_settings(ctx);
|
|
let read_files_workspace_allowlist = autonomy_settings.read_files_allowlist;
|
|
|
|
read_files_workspace_allowlist.unwrap_or_else(|| {
|
|
let profiles_model = AIExecutionProfilesModel::as_ref(ctx);
|
|
profiles_model
|
|
.get_profile_by_id(profile_id, ctx)
|
|
.unwrap_or_else(|| profiles_model.default_profile(ctx))
|
|
.data()
|
|
.directory_allowlist
|
|
.clone()
|
|
})
|
|
}
|
|
|
|
/// Returns an allowlist of paths that AM should be able to auto-read.
|
|
/// Note that the caller is responsible for deciding how the workspace's/user's settings
|
|
/// should affect how this gets used, if at all.
|
|
pub fn get_read_files_allowlist(
|
|
&self,
|
|
ctx: &AppContext,
|
|
terminal_view_id: Option<EntityId>,
|
|
) -> Vec<PathBuf> {
|
|
let active_profile =
|
|
AIExecutionProfilesModel::as_ref(ctx).active_profile(terminal_view_id, ctx);
|
|
self.get_read_files_allowlist_for_profile(ctx, *active_profile.id())
|
|
}
|
|
|
|
pub fn get_execute_commands_setting_for_profile(
|
|
&self,
|
|
ctx: &AppContext,
|
|
profile_id: ClientProfileId,
|
|
) -> ActionPermission {
|
|
let autonomy_settings = Self::workspace_autonomy_settings(ctx);
|
|
let execute_commands_workspace_setting = autonomy_settings.execute_commands_setting;
|
|
|
|
execute_commands_workspace_setting.unwrap_or_else(|| {
|
|
let profiles_model = AIExecutionProfilesModel::as_ref(ctx);
|
|
profiles_model
|
|
.get_profile_by_id(profile_id, ctx)
|
|
.unwrap_or_else(|| profiles_model.default_profile(ctx))
|
|
.data()
|
|
.execute_commands
|
|
})
|
|
}
|
|
|
|
/// Returns what the current setting is for executing commands,
|
|
/// based on the workspace setting and the active profile.
|
|
pub fn get_execute_commands_setting(
|
|
&self,
|
|
ctx: &AppContext,
|
|
terminal_view_id: Option<EntityId>,
|
|
) -> ActionPermission {
|
|
let active_profile =
|
|
AIExecutionProfilesModel::as_ref(ctx).active_profile(terminal_view_id, ctx);
|
|
self.get_execute_commands_setting_for_profile(ctx, *active_profile.id())
|
|
}
|
|
|
|
pub fn get_execute_commands_allowlist_for_profile(
|
|
&self,
|
|
ctx: &AppContext,
|
|
profile_id: ClientProfileId,
|
|
) -> Vec<AgentModeCommandExecutionPredicate> {
|
|
let autonomy_settings = Self::workspace_autonomy_settings(ctx);
|
|
let execute_commands_workspace_allowlist = autonomy_settings.execute_commands_allowlist;
|
|
|
|
execute_commands_workspace_allowlist.unwrap_or_else(|| {
|
|
let profiles_model = AIExecutionProfilesModel::as_ref(ctx);
|
|
profiles_model
|
|
.get_profile_by_id(profile_id, ctx)
|
|
.unwrap_or_else(|| profiles_model.default_profile(ctx))
|
|
.data()
|
|
.command_allowlist
|
|
.clone()
|
|
})
|
|
}
|
|
|
|
/// Returns an allowlist of command regexes that AM should be able to auto-execute.
|
|
/// Note that the caller is responsible for deciding how the workspace's/user's settings
|
|
/// should affect how this gets used, if at all.
|
|
pub fn get_execute_commands_allowlist(
|
|
&self,
|
|
ctx: &AppContext,
|
|
terminal_view_id: Option<EntityId>,
|
|
) -> Vec<AgentModeCommandExecutionPredicate> {
|
|
let active_profile =
|
|
AIExecutionProfilesModel::as_ref(ctx).active_profile(terminal_view_id, ctx);
|
|
self.get_execute_commands_allowlist_for_profile(ctx, *active_profile.id())
|
|
}
|
|
|
|
pub fn get_execute_commands_denylist_for_profile(
|
|
&self,
|
|
ctx: &AppContext,
|
|
profile_id: ClientProfileId,
|
|
) -> Vec<AgentModeCommandExecutionPredicate> {
|
|
let autonomy_settings = Self::workspace_autonomy_settings(ctx);
|
|
let profiles_model = AIExecutionProfilesModel::as_ref(ctx);
|
|
let user_denylist = profiles_model
|
|
.get_profile_by_id(profile_id, ctx)
|
|
.unwrap_or_else(|| profiles_model.default_profile(ctx))
|
|
.data()
|
|
.command_denylist
|
|
.clone();
|
|
|
|
match autonomy_settings.execute_commands_denylist {
|
|
Some(org_denylist) => {
|
|
let mut merged = org_denylist;
|
|
for item in user_denylist {
|
|
if !merged.contains(&item) {
|
|
merged.push(item);
|
|
}
|
|
}
|
|
merged
|
|
}
|
|
None => user_denylist,
|
|
}
|
|
}
|
|
|
|
pub fn get_org_execute_commands_denylist(
|
|
ctx: &AppContext,
|
|
) -> Vec<AgentModeCommandExecutionPredicate> {
|
|
let autonomy_settings = Self::workspace_autonomy_settings(ctx);
|
|
autonomy_settings
|
|
.execute_commands_denylist
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// Returns a denylist of command regexes that AM should not auto-execute.
|
|
/// Note that the caller is responsible for deciding how the workspace's/user's settings
|
|
/// should affect how this gets used, if at all.
|
|
pub fn get_execute_commands_denylist(
|
|
&self,
|
|
ctx: &AppContext,
|
|
terminal_view_id: Option<EntityId>,
|
|
) -> Vec<AgentModeCommandExecutionPredicate> {
|
|
let active_profile =
|
|
AIExecutionProfilesModel::as_ref(ctx).active_profile(terminal_view_id, ctx);
|
|
self.get_execute_commands_denylist_for_profile(ctx, *active_profile.id())
|
|
}
|
|
|
|
pub fn get_write_to_pty_setting_for_profile(
|
|
&self,
|
|
ctx: &AppContext,
|
|
profile_id: ClientProfileId,
|
|
) -> WriteToPtyPermission {
|
|
let autonomy_settings = Self::workspace_autonomy_settings(ctx);
|
|
let write_to_pty_workspace_setting = autonomy_settings.write_to_pty_setting;
|
|
|
|
write_to_pty_workspace_setting.unwrap_or_else(|| {
|
|
let profiles_model = AIExecutionProfilesModel::as_ref(ctx);
|
|
profiles_model
|
|
.get_profile_by_id(profile_id, ctx)
|
|
.unwrap_or_else(|| profiles_model.default_profile(ctx))
|
|
.data()
|
|
.write_to_pty
|
|
})
|
|
}
|
|
|
|
pub fn get_write_to_pty_setting(
|
|
&self,
|
|
ctx: &AppContext,
|
|
terminal_view_id: Option<EntityId>,
|
|
) -> WriteToPtyPermission {
|
|
let active_profile =
|
|
AIExecutionProfilesModel::as_ref(ctx).active_profile(terminal_view_id, ctx);
|
|
self.get_write_to_pty_setting_for_profile(ctx, *active_profile.id())
|
|
}
|
|
|
|
pub fn can_write_to_pty(
|
|
&self,
|
|
conversation_id: &AIConversationId,
|
|
terminal_view_id: Option<EntityId>,
|
|
ctx: &AppContext,
|
|
) -> WriteToPtyPermission {
|
|
if BlocklistAIHistoryModel::as_ref(ctx)
|
|
.conversation(conversation_id)
|
|
.is_some_and(|convo| convo.autoexecute_any_action())
|
|
{
|
|
return WriteToPtyPermission::AlwaysAllow;
|
|
}
|
|
self.get_write_to_pty_setting(ctx, terminal_view_id)
|
|
}
|
|
|
|
pub fn get_mcp_permissions_setting_for_profile(
|
|
&self,
|
|
ctx: &AppContext,
|
|
profile_id: ClientProfileId,
|
|
) -> ActionPermission {
|
|
// TODO: allow a workspace override on MCP permissions.
|
|
|
|
let profiles_model = AIExecutionProfilesModel::as_ref(ctx);
|
|
profiles_model
|
|
.get_profile_by_id(profile_id, ctx)
|
|
.unwrap_or_else(|| profiles_model.default_profile(ctx))
|
|
.data()
|
|
.mcp_permissions
|
|
}
|
|
|
|
pub fn get_mcp_permissions_setting(
|
|
&self,
|
|
ctx: &AppContext,
|
|
terminal_view_id: Option<EntityId>,
|
|
) -> ActionPermission {
|
|
let active_profile =
|
|
AIExecutionProfilesModel::as_ref(ctx).active_profile(terminal_view_id, ctx);
|
|
self.get_mcp_permissions_setting_for_profile(ctx, *active_profile.id())
|
|
}
|
|
|
|
pub fn get_mcp_allowlist_for_profile(
|
|
&self,
|
|
ctx: &AppContext,
|
|
profile_id: ClientProfileId,
|
|
) -> Vec<uuid::Uuid> {
|
|
// TODO: allow a workspace override on MCP allowlist.
|
|
|
|
let profiles_model = AIExecutionProfilesModel::as_ref(ctx);
|
|
profiles_model
|
|
.get_profile_by_id(profile_id, ctx)
|
|
.unwrap_or_else(|| profiles_model.default_profile(ctx))
|
|
.data()
|
|
.mcp_allowlist
|
|
.clone()
|
|
}
|
|
|
|
pub fn get_mcp_allowlist(
|
|
&self,
|
|
ctx: &AppContext,
|
|
terminal_view_id: Option<EntityId>,
|
|
) -> Vec<uuid::Uuid> {
|
|
let active_profile =
|
|
AIExecutionProfilesModel::as_ref(ctx).active_profile(terminal_view_id, ctx);
|
|
self.get_mcp_allowlist_for_profile(ctx, *active_profile.id())
|
|
}
|
|
|
|
pub fn get_mcp_denylist_for_profile(
|
|
&self,
|
|
ctx: &AppContext,
|
|
profile_id: ClientProfileId,
|
|
) -> Vec<uuid::Uuid> {
|
|
// TODO: allow a workspace override on MCP denylist.
|
|
|
|
let profiles_model = AIExecutionProfilesModel::as_ref(ctx);
|
|
profiles_model
|
|
.get_profile_by_id(profile_id, ctx)
|
|
.unwrap_or_else(|| profiles_model.default_profile(ctx))
|
|
.data()
|
|
.mcp_denylist
|
|
.clone()
|
|
}
|
|
|
|
pub fn get_mcp_denylist(
|
|
&self,
|
|
ctx: &AppContext,
|
|
terminal_view_id: Option<EntityId>,
|
|
) -> Vec<uuid::Uuid> {
|
|
let active_profile =
|
|
AIExecutionProfilesModel::as_ref(ctx).active_profile(terminal_view_id, ctx);
|
|
self.get_mcp_denylist_for_profile(ctx, *active_profile.id())
|
|
}
|
|
|
|
pub fn get_web_search_enabled_for_profile(
|
|
&self,
|
|
ctx: &AppContext,
|
|
profile_id: ClientProfileId,
|
|
) -> bool {
|
|
let profiles_model = AIExecutionProfilesModel::as_ref(ctx);
|
|
profiles_model
|
|
.get_profile_by_id(profile_id, ctx)
|
|
.unwrap_or_else(|| profiles_model.default_profile(ctx))
|
|
.data()
|
|
.web_search_enabled
|
|
}
|
|
|
|
pub fn get_web_search_enabled(
|
|
&self,
|
|
ctx: &AppContext,
|
|
terminal_view_id: Option<EntityId>,
|
|
) -> bool {
|
|
let active_profile =
|
|
AIExecutionProfilesModel::as_ref(ctx).active_profile(terminal_view_id, ctx);
|
|
self.get_web_search_enabled_for_profile(ctx, *active_profile.id())
|
|
}
|
|
|
|
pub fn get_computer_use_setting_for_profile(
|
|
&self,
|
|
ctx: &AppContext,
|
|
profile_id: ClientProfileId,
|
|
) -> crate::ai::execution_profiles::ComputerUsePermission {
|
|
let autonomy_settings = Self::workspace_autonomy_settings(ctx);
|
|
let computer_use_workspace_setting = autonomy_settings.computer_use_setting;
|
|
|
|
computer_use_workspace_setting.unwrap_or_else(|| {
|
|
let profiles_model = AIExecutionProfilesModel::as_ref(ctx);
|
|
profiles_model
|
|
.get_profile_by_id(profile_id, ctx)
|
|
.unwrap_or_else(|| profiles_model.default_profile(ctx))
|
|
.data()
|
|
.computer_use
|
|
})
|
|
}
|
|
|
|
pub fn get_computer_use_setting(
|
|
&self,
|
|
ctx: &AppContext,
|
|
terminal_view_id: Option<EntityId>,
|
|
) -> crate::ai::execution_profiles::ComputerUsePermission {
|
|
let active_profile =
|
|
AIExecutionProfilesModel::as_ref(ctx).active_profile(terminal_view_id, ctx);
|
|
self.get_computer_use_setting_for_profile(ctx, *active_profile.id())
|
|
}
|
|
|
|
pub fn get_ask_user_question_setting_for_profile(
|
|
&self,
|
|
ctx: &AppContext,
|
|
profile_id: ClientProfileId,
|
|
) -> AskUserQuestionPermission {
|
|
let profiles_model = AIExecutionProfilesModel::as_ref(ctx);
|
|
profiles_model
|
|
.get_profile_by_id(profile_id, ctx)
|
|
.unwrap_or_else(|| profiles_model.default_profile(ctx))
|
|
.data()
|
|
.ask_user_question
|
|
}
|
|
|
|
pub fn get_ask_user_question_setting(
|
|
&self,
|
|
ctx: &AppContext,
|
|
terminal_view_id: Option<EntityId>,
|
|
) -> AskUserQuestionPermission {
|
|
let active_profile =
|
|
AIExecutionProfilesModel::as_ref(ctx).active_profile(terminal_view_id, ctx);
|
|
self.get_ask_user_question_setting_for_profile(ctx, *active_profile.id())
|
|
}
|
|
|
|
pub fn get_run_agents_setting_for_profile(
|
|
&self,
|
|
ctx: &AppContext,
|
|
profile_id: ClientProfileId,
|
|
) -> crate::ai::execution_profiles::RunAgentsPermission {
|
|
let profiles_model = AIExecutionProfilesModel::as_ref(ctx);
|
|
profiles_model
|
|
.get_profile_by_id(profile_id, ctx)
|
|
.unwrap_or_else(|| profiles_model.default_profile(ctx))
|
|
.data()
|
|
.run_agents
|
|
}
|
|
|
|
pub fn get_run_agents_setting(
|
|
&self,
|
|
ctx: &AppContext,
|
|
terminal_view_id: Option<EntityId>,
|
|
) -> crate::ai::execution_profiles::RunAgentsPermission {
|
|
let active_profile =
|
|
AIExecutionProfilesModel::as_ref(ctx).active_profile(terminal_view_id, ctx);
|
|
self.get_run_agents_setting_for_profile(ctx, *active_profile.id())
|
|
}
|
|
|
|
/// Returns whether or not Agent Mode can auto-read the given files.
|
|
pub fn can_read_files_with_conversation(
|
|
&self,
|
|
conversation_id: &AIConversationId,
|
|
paths: Vec<PathBuf>,
|
|
terminal_view_id: Option<EntityId>,
|
|
ctx: &AppContext,
|
|
) -> FileReadPermission {
|
|
if BlocklistAIHistoryModel::as_ref(ctx)
|
|
.conversation(conversation_id)
|
|
.is_some_and(|convo| convo.autoexecute_any_action())
|
|
{
|
|
return FileReadPermission::Allowed(FileReadPermissionAllowedReason::RunToCompletion);
|
|
}
|
|
|
|
self.can_read_files(Some(conversation_id), paths, terminal_view_id, ctx)
|
|
}
|
|
|
|
/// Returns whether or not Warp can auto-read the given files (e.g. for codebase indexing).
|
|
pub fn can_read_files(
|
|
&self,
|
|
conversation_id: Option<&AIConversationId>,
|
|
paths: Vec<PathBuf>,
|
|
terminal_view_id: Option<EntityId>,
|
|
ctx: &AppContext,
|
|
) -> FileReadPermission {
|
|
if paths.is_empty() {
|
|
// We can vacuously read 0 files.
|
|
return FileReadPermission::Allowed(
|
|
FileReadPermissionAllowedReason::ExplicitlyAllowlisted,
|
|
);
|
|
}
|
|
|
|
// Check if we've already been given permission to read these files in this conversation.
|
|
if let Some(temp_permissions) =
|
|
conversation_id.and_then(|id| self.temporary_file_permissions.get(id))
|
|
{
|
|
if paths.iter().all(|path| {
|
|
temp_permissions
|
|
.iter()
|
|
.any(|allowed| path.starts_with(allowed))
|
|
}) {
|
|
return FileReadPermission::Allowed(
|
|
FileReadPermissionAllowedReason::AlreadyReadInConvo,
|
|
);
|
|
}
|
|
}
|
|
|
|
match self.get_read_files_setting(ctx, terminal_view_id) {
|
|
ActionPermission::AgentDecides | ActionPermission::Unknown => {
|
|
// For now, we always read files. We don't ask the user for permission.
|
|
FileReadPermission::Allowed(FileReadPermissionAllowedReason::AgentDecided)
|
|
}
|
|
ActionPermission::AlwaysAllow => {
|
|
FileReadPermission::Allowed(FileReadPermissionAllowedReason::AutoreadSettingEnabled)
|
|
}
|
|
ActionPermission::AlwaysAsk => {
|
|
let allowlisted_paths = self.get_read_files_allowlist(ctx, terminal_view_id);
|
|
if paths
|
|
.iter()
|
|
.all(|p| allowlisted_paths.iter().any(|dir| p.starts_with(dir)))
|
|
{
|
|
FileReadPermission::Allowed(
|
|
FileReadPermissionAllowedReason::ExplicitlyAllowlisted,
|
|
)
|
|
} else {
|
|
FileReadPermission::Denied(FileReadPermissionDeniedReason::AlwaysAskEnabled)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns whether or not Agent Mode can automatically write to files.
|
|
pub fn can_write_files(
|
|
&self,
|
|
conversation_id: &AIConversationId,
|
|
paths: &[PathBuf],
|
|
terminal_view_id: Option<EntityId>,
|
|
ctx: &AppContext,
|
|
) -> FileWritePermission {
|
|
// Protected paths are always denied, regardless of autonomy settings.
|
|
if let Some(denied) = check_protected_write_paths(paths) {
|
|
return denied;
|
|
}
|
|
|
|
if BlocklistAIHistoryModel::as_ref(ctx)
|
|
.conversation(conversation_id)
|
|
.is_some_and(|convo| convo.autoexecute_any_action())
|
|
{
|
|
return FileWritePermission::Allowed(FileWritePermissionAllowedReason::RunToCompletion);
|
|
}
|
|
|
|
self.determine_write_permissions_from_active_profile(terminal_view_id, ctx)
|
|
}
|
|
|
|
#[cfg(not(target_family = "wasm"))]
|
|
pub fn can_call_mcp_tool(
|
|
&self,
|
|
server_id: Option<&uuid::Uuid>,
|
|
name: &str,
|
|
conversation_id: &AIConversationId,
|
|
terminal_view_id: Option<EntityId>,
|
|
ctx: &AppContext,
|
|
) -> bool {
|
|
let templatable_manager = TemplatableMCPServerManager::as_ref(ctx);
|
|
|
|
// Try resolving via server UUID first, then fall back to tool-name lookup.
|
|
// On recent clients, the server UUID should always be set - we should eventually
|
|
// require the server UUID.
|
|
let mut uuid_of_mcp_server =
|
|
server_id.and_then(|id| templatable_manager.get_template_uuid(*id));
|
|
|
|
// Prefer templatable MCP servers over legacy when a tool name exists in both.
|
|
// Fall back to legacy behavior if templatable lookup fails or is disabled.
|
|
if uuid_of_mcp_server.is_none() {
|
|
uuid_of_mcp_server = templatable_manager
|
|
.server_from_tool(name.to_string())
|
|
.copied()
|
|
.and_then(|installation_uuid| {
|
|
templatable_manager.get_template_uuid(installation_uuid)
|
|
});
|
|
}
|
|
|
|
self.can_use_mcp_server(conversation_id, uuid_of_mcp_server, terminal_view_id, ctx)
|
|
}
|
|
|
|
/// Returns whether or not Agent Mode can automatically read the given MCP resource.
|
|
#[cfg(not(target_family = "wasm"))]
|
|
pub fn can_read_mcp_resource(
|
|
&self,
|
|
server_id: Option<&uuid::Uuid>,
|
|
name: &str,
|
|
uri: Option<&str>,
|
|
conversation_id: &AIConversationId,
|
|
terminal_view_id: Option<EntityId>,
|
|
ctx: &AppContext,
|
|
) -> bool {
|
|
let templatable_manager = TemplatableMCPServerManager::as_ref(ctx);
|
|
|
|
// Try resolving via server UUID first, then fall back to resource name/URI lookup.
|
|
// On recent clients, the server UUID should always be set - we should eventually
|
|
// require the server UUID.
|
|
let mut uuid_of_mcp_server =
|
|
server_id.and_then(|id| templatable_manager.get_template_uuid(*id));
|
|
|
|
// Prefer templatable MCP servers over legacy when a resource name exists in both.
|
|
// Fall back to legacy behavior if templatable lookup fails or is disabled.
|
|
if uuid_of_mcp_server.is_none() {
|
|
uuid_of_mcp_server = templatable_manager
|
|
.server_from_resource(name, uri)
|
|
.copied()
|
|
.and_then(|installation_uuid| {
|
|
templatable_manager.get_template_uuid(installation_uuid)
|
|
});
|
|
}
|
|
|
|
self.can_use_mcp_server(conversation_id, uuid_of_mcp_server, terminal_view_id, ctx)
|
|
}
|
|
|
|
/// Checks whether the given MCP server (identified by its template UUID) is permitted
|
|
/// to be used based on the current MCP permission setting and allowlist/denylist.
|
|
#[cfg(not(target_family = "wasm"))]
|
|
fn can_use_mcp_server(
|
|
&self,
|
|
conversation_id: &AIConversationId,
|
|
uuid_of_mcp_server: Option<uuid::Uuid>,
|
|
terminal_view_id: Option<EntityId>,
|
|
ctx: &AppContext,
|
|
) -> bool {
|
|
if BlocklistAIHistoryModel::as_ref(ctx)
|
|
.conversation(conversation_id)
|
|
.is_some_and(|convo| convo.autoexecute_any_action())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
let allowlisted = uuid_of_mcp_server
|
|
.is_some_and(|uid| self.get_mcp_allowlist(ctx, terminal_view_id).contains(&uid));
|
|
let denylisted = uuid_of_mcp_server
|
|
.is_some_and(|uid| self.get_mcp_denylist(ctx, terminal_view_id).contains(&uid));
|
|
|
|
match self.get_mcp_permissions_setting(ctx, terminal_view_id) {
|
|
ActionPermission::AgentDecides | ActionPermission::Unknown => {
|
|
allowlisted && !denylisted
|
|
}
|
|
ActionPermission::AlwaysAllow => !denylisted,
|
|
ActionPermission::AlwaysAsk => allowlisted && !denylisted,
|
|
}
|
|
}
|
|
|
|
// Helper function to evaluate the active profile + workspace settings.
|
|
fn determine_write_permissions_from_active_profile(
|
|
&self,
|
|
terminal_view_id: Option<EntityId>,
|
|
ctx: &AppContext,
|
|
) -> FileWritePermission {
|
|
match self.get_apply_code_diffs_setting(ctx, terminal_view_id) {
|
|
ActionPermission::AgentDecides | ActionPermission::Unknown => {
|
|
FileWritePermission::Denied(FileWritePermissionDeniedReason::AgentDecided)
|
|
}
|
|
ActionPermission::AlwaysAllow => FileWritePermission::Allowed(
|
|
FileWritePermissionAllowedReason::AutowriteSettingEnabled,
|
|
),
|
|
ActionPermission::AlwaysAsk => {
|
|
FileWritePermission::Denied(FileWritePermissionDeniedReason::AlwaysAskEnabled)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
/// Returns whether or not Agent Mode can auto-execute the given command.
|
|
pub fn can_autoexecute_command(
|
|
&self,
|
|
conversation_id: &AIConversationId,
|
|
command: &str,
|
|
escape_char: EscapeChar,
|
|
is_read_only: bool,
|
|
is_risky: Option<bool>,
|
|
terminal_view_id: Option<EntityId>,
|
|
ctx: &AppContext,
|
|
) -> CommandExecutionPermission {
|
|
// Normalize line continuations based on shell type.
|
|
// POSIX shells (bash/zsh/fish) use backslash, PowerShell uses backtick.
|
|
let normalized_command = match escape_char {
|
|
EscapeChar::Backslash => command.replace("\\\n", " "),
|
|
EscapeChar::Backtick => command.replace("`\n", " "),
|
|
};
|
|
|
|
// The command string might be composed of multiple commands so let's
|
|
// break it up first.
|
|
let (commands, contains_redirection) = decompose_command(&normalized_command, escape_char);
|
|
let commands_for_denylist = commands
|
|
.iter()
|
|
.map(|command| command_for_execution_predicates(command, escape_char))
|
|
.collect::<Vec<_>>();
|
|
|
|
// The denylist takes precedence over all other conditions.
|
|
let denylist = self.get_execute_commands_denylist(ctx, terminal_view_id);
|
|
if commands_for_denylist
|
|
.iter()
|
|
.any(|c| denylist.iter().any(|d| d.matches(c)))
|
|
{
|
|
return CommandExecutionPermission::Denied(
|
|
CommandExecutionPermissionDeniedReason::ExplicitlyDenylisted,
|
|
);
|
|
}
|
|
|
|
if BlocklistAIHistoryModel::as_ref(ctx)
|
|
.conversation(conversation_id)
|
|
.is_some_and(|convo| convo.autoexecute_any_action())
|
|
{
|
|
return CommandExecutionPermission::Allowed(
|
|
CommandExecutionPermissionAllowedReason::RunToCompletion,
|
|
);
|
|
}
|
|
|
|
match self.get_execute_commands_setting(ctx, terminal_view_id) {
|
|
ActionPermission::AgentDecides | ActionPermission::Unknown => {
|
|
if FeatureFlag::AgentDecidesCommandExecution.is_enabled() && is_risky == Some(false)
|
|
{
|
|
return CommandExecutionPermission::Allowed(
|
|
CommandExecutionPermissionAllowedReason::AgentDecided,
|
|
);
|
|
}
|
|
|
|
if contains_redirection {
|
|
return CommandExecutionPermission::Denied(
|
|
CommandExecutionPermissionDeniedReason::ContainsRedirection,
|
|
);
|
|
}
|
|
|
|
let allowlist = self.get_execute_commands_allowlist(ctx, terminal_view_id);
|
|
if commands.iter().all(|command| {
|
|
allowlist
|
|
.iter()
|
|
.any(|allowlist_item| allowlist_item.matches(command))
|
|
}) {
|
|
return CommandExecutionPermission::Allowed(
|
|
CommandExecutionPermissionAllowedReason::ExplicitlyAllowlisted,
|
|
);
|
|
}
|
|
|
|
// For now, the heuristic is if the command is read only or if we're executing
|
|
// a plan. Otherwise, we don't want to autoexecute.
|
|
if is_read_only {
|
|
CommandExecutionPermission::Allowed(
|
|
CommandExecutionPermissionAllowedReason::AgentDecided,
|
|
)
|
|
} else {
|
|
CommandExecutionPermission::Denied(
|
|
CommandExecutionPermissionDeniedReason::AgentDecided,
|
|
)
|
|
}
|
|
}
|
|
ActionPermission::AlwaysAllow => CommandExecutionPermission::Allowed(
|
|
CommandExecutionPermissionAllowedReason::AlwaysAllowed,
|
|
),
|
|
ActionPermission::AlwaysAsk => {
|
|
let allowlist = self.get_execute_commands_allowlist(ctx, terminal_view_id);
|
|
|
|
if commands.iter().all(|command| {
|
|
allowlist
|
|
.iter()
|
|
.any(|allowlist_item| allowlist_item.matches(command))
|
|
}) {
|
|
CommandExecutionPermission::Allowed(
|
|
CommandExecutionPermissionAllowedReason::ExplicitlyAllowlisted,
|
|
)
|
|
} else {
|
|
CommandExecutionPermission::Denied(
|
|
CommandExecutionPermissionDeniedReason::AlwaysAskEnabled,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Allows Agent Mode to auto-execute commands that match `command`.
|
|
///
|
|
/// The denylist (see [`Self::add_command_to_autoexecution_denylist`])
|
|
/// takes precedence over the allowlist.
|
|
pub fn add_command_to_autoexecution_allowlist(
|
|
&mut self,
|
|
command: AgentModeCommandExecutionPredicate,
|
|
ctx: &mut ModelContext<Self>,
|
|
) -> Result<()> {
|
|
let mut allowlist = AISettings::as_ref(ctx)
|
|
.agent_mode_command_execution_allowlist
|
|
.clone();
|
|
allowlist.push(command);
|
|
AISettings::handle(ctx).update(ctx, |settings, ctx| {
|
|
settings
|
|
.agent_mode_command_execution_allowlist
|
|
.set_value(allowlist, ctx)
|
|
})
|
|
}
|
|
|
|
/// Removes `command` from the auto-execution allowlist.
|
|
///
|
|
/// See [`Self::add_command_to_autoexecution_allowlist`] for more about the allowlist.
|
|
pub fn remove_command_from_autoexecution_allowlist(
|
|
&mut self,
|
|
command: &AgentModeCommandExecutionPredicate,
|
|
ctx: &mut ModelContext<Self>,
|
|
) -> Result<()> {
|
|
let mut allowlist = AISettings::as_ref(ctx)
|
|
.agent_mode_command_execution_allowlist
|
|
.clone();
|
|
allowlist.retain(|c| c != command);
|
|
AISettings::handle(ctx).update(ctx, |settings, ctx| {
|
|
settings
|
|
.agent_mode_command_execution_allowlist
|
|
.set_value(allowlist, ctx)
|
|
})
|
|
}
|
|
|
|
/// Forces Agent Mode to ask for user consent before executing commands that match `command`.
|
|
pub fn add_command_to_autoexecution_denylist(
|
|
&mut self,
|
|
command: AgentModeCommandExecutionPredicate,
|
|
ctx: &mut ModelContext<Self>,
|
|
) -> Result<()> {
|
|
let mut denylist = AISettings::as_ref(ctx)
|
|
.agent_mode_command_execution_denylist
|
|
.clone();
|
|
denylist.push(command);
|
|
AISettings::handle(ctx).update(ctx, |settings, ctx| {
|
|
settings
|
|
.agent_mode_command_execution_denylist
|
|
.set_value(denylist, ctx)
|
|
})
|
|
}
|
|
|
|
/// Removes `command` from the auto-execution denylist.
|
|
///
|
|
/// See [`Self::add_command_to_autoexecution_denylist`] for more about the denylist.
|
|
pub fn remove_command_from_denylist(
|
|
&mut self,
|
|
command: &AgentModeCommandExecutionPredicate,
|
|
ctx: &mut ModelContext<Self>,
|
|
) -> Result<()> {
|
|
let mut denylist = AISettings::as_ref(ctx)
|
|
.agent_mode_command_execution_denylist
|
|
.clone();
|
|
denylist.retain(|c| c != command);
|
|
AISettings::handle(ctx).update(ctx, |settings, ctx| {
|
|
settings
|
|
.agent_mode_command_execution_denylist
|
|
.set_value(denylist, ctx)
|
|
})
|
|
}
|
|
|
|
/// Sets whether or not readonly commands can be auto-executed by Agent Mode.
|
|
pub fn set_should_autoexecute_readonly_commands(
|
|
&mut self,
|
|
enabled: bool,
|
|
ctx: &mut ModelContext<Self>,
|
|
) -> Result<()> {
|
|
AISettings::handle(ctx).update(ctx, |settings, ctx| {
|
|
settings
|
|
.agent_mode_execute_read_only_commands
|
|
.set_value(enabled, ctx)
|
|
.map(|_| ())?;
|
|
|
|
// If enabling, no need to show the file speedbump since
|
|
// that setting will be superseded by this setting.
|
|
if enabled {
|
|
settings
|
|
.should_show_agent_mode_autoread_files_speedbump
|
|
.set_value(false, ctx)?;
|
|
}
|
|
|
|
settings
|
|
.should_show_agent_mode_autoexecute_readonly_commands_speedbump
|
|
.set_value(false, ctx)
|
|
})
|
|
}
|
|
|
|
/// Sets whether or not we should always allow writing to the PTY.
|
|
pub fn set_always_allow_write_to_pty(
|
|
&mut self,
|
|
enabled: bool,
|
|
terminal_view_id: EntityId,
|
|
ctx: &mut ModelContext<Self>,
|
|
) -> Result<()> {
|
|
let permission = if enabled {
|
|
WriteToPtyPermission::AlwaysAllow
|
|
} else {
|
|
WriteToPtyPermission::AlwaysAsk
|
|
};
|
|
let active_profile =
|
|
AIExecutionProfilesModel::as_ref(ctx).active_profile(Some(terminal_view_id), ctx);
|
|
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
|
profiles_model.set_write_to_pty(*active_profile.id(), &permission, ctx);
|
|
});
|
|
Ok(())
|
|
}
|
|
|
|
/// Sets whether or not we should always allow reading files.
|
|
pub fn set_always_allow_read_files(
|
|
&mut self,
|
|
enabled: bool,
|
|
terminal_view_id: EntityId,
|
|
ctx: &mut ModelContext<Self>,
|
|
) -> Result<()> {
|
|
let permissions = if enabled {
|
|
ActionPermission::AlwaysAllow
|
|
} else {
|
|
ActionPermission::AlwaysAsk
|
|
};
|
|
|
|
let active_profile =
|
|
AIExecutionProfilesModel::as_ref(ctx).active_profile(Some(terminal_view_id), ctx);
|
|
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles_model, ctx| {
|
|
profiles_model.set_read_files(*active_profile.id(), &permissions, ctx);
|
|
});
|
|
Ok(())
|
|
}
|
|
|
|
/// Sets permissions that Agent Mode has for coding tasks.
|
|
pub fn set_coding_permissions(
|
|
&mut self,
|
|
permissions: AgentModeCodingPermissionsType,
|
|
ctx: &mut ModelContext<Self>,
|
|
) -> Result<()> {
|
|
AISettings::handle(ctx).update(ctx, |settings, ctx| {
|
|
settings
|
|
.agent_mode_coding_permissions
|
|
.set_value(permissions, ctx)
|
|
.map(|_| ())?;
|
|
|
|
settings
|
|
.should_show_agent_mode_autoread_files_speedbump
|
|
.set_value(false, ctx)
|
|
})
|
|
}
|
|
|
|
/// Adds a filepath that Agent Mode can read for coding tasks without additional permissions.
|
|
/// Used in conjunction with [`AgentModeCodingPermissionsType::AllowReadingSpecificFiles`].
|
|
///
|
|
/// This does not do any validation on the filepath; callers should ensure the filepath is valid.
|
|
pub fn add_filepath_to_code_read_allowlist(
|
|
&mut self,
|
|
filepath: PathBuf,
|
|
ctx: &mut ModelContext<Self>,
|
|
) -> Result<()> {
|
|
let mut allowlist = AISettings::as_ref(ctx)
|
|
.agent_mode_coding_file_read_allowlist
|
|
.clone();
|
|
allowlist.push(filepath);
|
|
AISettings::handle(ctx).update(ctx, |settings, ctx| {
|
|
settings
|
|
.agent_mode_coding_file_read_allowlist
|
|
.set_value(allowlist, ctx)
|
|
})
|
|
}
|
|
|
|
/// Counterpart to [`Self::add_filepath_to_code_read_allowlist`].
|
|
pub fn remove_filepath_from_code_read_allowlist(
|
|
&mut self,
|
|
filepath: PathBuf,
|
|
ctx: &mut ModelContext<Self>,
|
|
) -> Result<()> {
|
|
let mut allowlist = AISettings::as_ref(ctx)
|
|
.agent_mode_coding_file_read_allowlist
|
|
.clone();
|
|
allowlist.retain(|p| p != &filepath);
|
|
AISettings::handle(ctx).update(ctx, |settings, ctx| {
|
|
settings
|
|
.agent_mode_coding_file_read_allowlist
|
|
.set_value(allowlist, ctx)
|
|
})
|
|
}
|
|
|
|
/// Gives Agent Mode temporary access to the provided `files`.
|
|
/// The permissions are scoped to the given conversation.
|
|
pub fn add_temporary_file_read_permissions<P: Into<PathBuf>>(
|
|
&mut self,
|
|
conversation_id: AIConversationId,
|
|
files: impl IntoIterator<Item = P>,
|
|
) {
|
|
self.temporary_file_permissions
|
|
.entry(conversation_id)
|
|
.or_default()
|
|
.extend(files.into_iter().map(Into::into));
|
|
}
|
|
|
|
/// Returns whether the agent can ask the user a question in the given conversation.
|
|
pub fn can_ask_user_question(
|
|
&self,
|
|
conversation_id: &AIConversationId,
|
|
terminal_view_id: Option<EntityId>,
|
|
ctx: &AppContext,
|
|
) -> bool {
|
|
match self.get_ask_user_question_setting(ctx, terminal_view_id) {
|
|
AskUserQuestionPermission::Never => false,
|
|
AskUserQuestionPermission::AskExceptInAutoApprove
|
|
| AskUserQuestionPermission::Unknown => !BlocklistAIHistoryModel::as_ref(ctx)
|
|
.conversation(conversation_id)
|
|
.is_some_and(|convo| convo.autoexecute_any_action()),
|
|
AskUserQuestionPermission::AlwaysAsk => true,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn command_for_execution_predicates(command: &str, escape_char: EscapeChar) -> String {
|
|
command_without_leading_env_vars(command, escape_char)
|
|
.filter(|command| !command.is_empty())
|
|
.unwrap_or_else(|| command.to_string())
|
|
}
|
|
|
|
/// Returns `Some(Denied(ProtectedPath))` if any of the given paths are system-protected
|
|
/// and must never be auto-written regardless of user autonomy settings.
|
|
/// Returns `None` if no paths are protected.
|
|
fn check_protected_write_paths(paths: &[PathBuf]) -> Option<FileWritePermission> {
|
|
// MCP config files are always protected from auto-write to prevent security risks
|
|
// from injecting arbitrary context into the agent.
|
|
if paths
|
|
.iter()
|
|
.any(|p| mcp_provider_from_file_path(p).is_some())
|
|
{
|
|
Some(FileWritePermission::Denied(
|
|
FileWritePermissionDeniedReason::ProtectedPath,
|
|
))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
impl Entity for BlocklistAIPermissions {
|
|
type Event = ();
|
|
}
|
|
|
|
impl SingletonEntity for BlocklistAIPermissions {}
|
|
|
|
/// Returns true iff Agent Mode autonomy features are allowed on this client.
|
|
/// Granular permissions still need to be checked for specific autonomy features
|
|
/// (e.g. whether a command is auto-executable).
|
|
pub fn is_agent_mode_autonomy_allowed(ctx: &AppContext) -> bool {
|
|
crate::UserWorkspaces::as_ref(ctx).is_ai_autonomy_allowed()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "permissions_tests.rs"]
|
|
mod tests;
|