Addressing the feedback left on a previous PR of mine: https://github.com/zed-industries/zed/pull/46663. I feel like this is worthy of a setting. Note, however, that this does not apply to typing `ctrl-c` in the terminal from within the agent panel; this is purely controlling what happens when you click on the "Stop" button in the terminal tool call card. Release Notes: - Agent: Added a setting for controlling the behavior of the stop button in the terminal tool call card (between cancelling the command to run and the thread, or just the command).
795 lines
29 KiB
Rust
795 lines
29 KiB
Rust
use collections::{HashMap, IndexMap};
|
|
use schemars::{JsonSchema, json_schema};
|
|
use serde::{Deserialize, Serialize};
|
|
use settings_macros::{MergeFrom, with_fallible_options};
|
|
use std::sync::Arc;
|
|
use std::{borrow::Cow, path::PathBuf};
|
|
|
|
use crate::ExtendingVec;
|
|
|
|
use crate::{DockPosition, DockSide};
|
|
|
|
#[with_fallible_options]
|
|
#[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, Default)]
|
|
pub struct AgentSettingsContent {
|
|
/// Whether the Agent is enabled.
|
|
///
|
|
/// Default: true
|
|
pub enabled: Option<bool>,
|
|
/// Whether to show the agent panel button in the status bar.
|
|
///
|
|
/// Default: true
|
|
pub button: Option<bool>,
|
|
/// Where to dock the agent panel.
|
|
///
|
|
/// Default: right
|
|
pub dock: Option<DockPosition>,
|
|
/// Where to dock the utility pane (the thread view pane).
|
|
///
|
|
/// Default: left
|
|
pub agents_panel_dock: Option<DockSide>,
|
|
/// Default width in pixels when the agent panel is docked to the left or right.
|
|
///
|
|
/// Default: 640
|
|
#[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
|
|
pub default_width: Option<f32>,
|
|
/// Default height in pixels when the agent panel is docked to the bottom.
|
|
///
|
|
/// Default: 320
|
|
#[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
|
|
pub default_height: Option<f32>,
|
|
/// The default model to use when creating new chats and for other features when a specific model is not specified.
|
|
pub default_model: Option<LanguageModelSelection>,
|
|
/// Favorite models to show at the top of the model selector.
|
|
#[serde(default)]
|
|
pub favorite_models: Vec<LanguageModelSelection>,
|
|
/// Model to use for the inline assistant. Defaults to default_model when not specified.
|
|
pub inline_assistant_model: Option<LanguageModelSelection>,
|
|
/// Model to use for the inline assistant when streaming tools are enabled.
|
|
///
|
|
/// Default: true
|
|
pub inline_assistant_use_streaming_tools: Option<bool>,
|
|
/// Model to use for generating git commit messages.
|
|
///
|
|
/// Default: true
|
|
pub commit_message_model: Option<LanguageModelSelection>,
|
|
/// Model to use for generating thread summaries. Defaults to default_model when not specified.
|
|
pub thread_summary_model: Option<LanguageModelSelection>,
|
|
/// Additional models with which to generate alternatives when performing inline assists.
|
|
pub inline_alternatives: Option<Vec<LanguageModelSelection>>,
|
|
/// The default profile to use in the Agent.
|
|
///
|
|
/// Default: write
|
|
pub default_profile: Option<Arc<str>>,
|
|
/// Which view type to show by default in the agent panel.
|
|
///
|
|
/// Default: "thread"
|
|
pub default_view: Option<DefaultAgentView>,
|
|
/// The available agent profiles.
|
|
pub profiles: Option<IndexMap<Arc<str>, AgentProfileContent>>,
|
|
/// Whenever a tool action would normally wait for your confirmation
|
|
/// that you allow it, always choose to allow it.
|
|
///
|
|
/// This setting has no effect on external agents that support permission modes, such as Claude Code.
|
|
///
|
|
/// Set `agent_servers.claude.default_mode` to `bypassPermissions`, to disable all permission requests when using Claude Code.
|
|
///
|
|
/// Default: false
|
|
pub always_allow_tool_actions: Option<bool>,
|
|
/// Where to show a popup notification when the agent is waiting for user input.
|
|
///
|
|
/// Default: "primary_screen"
|
|
pub notify_when_agent_waiting: Option<NotifyWhenAgentWaiting>,
|
|
/// Whether to play a sound when the agent has either completed its response, or needs user input.
|
|
///
|
|
/// Default: false
|
|
pub play_sound_when_agent_done: Option<bool>,
|
|
/// Whether to display agent edits in single-file editors in addition to the review multibuffer pane.
|
|
///
|
|
/// Default: true
|
|
pub single_file_review: Option<bool>,
|
|
/// Additional parameters for language model requests. When making a request
|
|
/// to a model, parameters will be taken from the last entry in this list
|
|
/// that matches the model's provider and name. In each entry, both provider
|
|
/// and model are optional, so that you can specify parameters for either
|
|
/// one.
|
|
///
|
|
/// Default: []
|
|
#[serde(default)]
|
|
pub model_parameters: Vec<LanguageModelParameters>,
|
|
/// Whether to show thumb buttons for feedback in the agent panel.
|
|
///
|
|
/// Default: true
|
|
pub enable_feedback: Option<bool>,
|
|
/// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff.
|
|
///
|
|
/// Default: true
|
|
pub expand_edit_card: Option<bool>,
|
|
/// Whether to have terminal cards in the agent panel expanded, showing the whole command output.
|
|
///
|
|
/// Default: true
|
|
pub expand_terminal_card: Option<bool>,
|
|
/// Whether clicking the stop button on a running terminal tool should also cancel the agent's generation.
|
|
/// Note that this only applies to the stop button, not to ctrl+c inside the terminal.
|
|
///
|
|
/// Default: true
|
|
pub cancel_generation_on_terminal_stop: Option<bool>,
|
|
/// Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel.
|
|
///
|
|
/// Default: false
|
|
pub use_modifier_to_send: Option<bool>,
|
|
/// Minimum number of lines of height the agent message editor should have.
|
|
///
|
|
/// Default: 4
|
|
pub message_editor_min_lines: Option<usize>,
|
|
/// Whether to show turn statistics (elapsed time during generation, final turn duration).
|
|
///
|
|
/// Default: false
|
|
pub show_turn_stats: Option<bool>,
|
|
/// Per-tool permission rules for granular control over which tool actions require confirmation.
|
|
///
|
|
/// This setting only applies to the native Zed agent. External agent servers (Claude Code, Gemini CLI, etc.)
|
|
/// have their own permission systems and are not affected by these settings.
|
|
pub tool_permissions: Option<ToolPermissionsContent>,
|
|
}
|
|
|
|
impl AgentSettingsContent {
|
|
pub fn set_dock(&mut self, dock: DockPosition) {
|
|
self.dock = Some(dock);
|
|
}
|
|
|
|
pub fn set_model(&mut self, language_model: LanguageModelSelection) {
|
|
// let model = language_model.id().0.to_string();
|
|
// let provider = language_model.provider_id().0.to_string();
|
|
// self.default_model = Some(LanguageModelSelection {
|
|
// provider: provider.into(),
|
|
// model,
|
|
// });
|
|
self.default_model = Some(language_model)
|
|
}
|
|
|
|
pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
|
|
self.inline_assistant_model = Some(LanguageModelSelection {
|
|
provider: provider.into(),
|
|
model,
|
|
});
|
|
}
|
|
pub fn set_inline_assistant_use_streaming_tools(&mut self, use_tools: bool) {
|
|
self.inline_assistant_use_streaming_tools = Some(use_tools);
|
|
}
|
|
|
|
pub fn set_commit_message_model(&mut self, provider: String, model: String) {
|
|
self.commit_message_model = Some(LanguageModelSelection {
|
|
provider: provider.into(),
|
|
model,
|
|
});
|
|
}
|
|
|
|
pub fn set_thread_summary_model(&mut self, provider: String, model: String) {
|
|
self.thread_summary_model = Some(LanguageModelSelection {
|
|
provider: provider.into(),
|
|
model,
|
|
});
|
|
}
|
|
|
|
pub fn set_always_allow_tool_actions(&mut self, allow: bool) {
|
|
self.always_allow_tool_actions = Some(allow);
|
|
}
|
|
|
|
pub fn set_play_sound_when_agent_done(&mut self, allow: bool) {
|
|
self.play_sound_when_agent_done = Some(allow);
|
|
}
|
|
|
|
pub fn set_single_file_review(&mut self, allow: bool) {
|
|
self.single_file_review = Some(allow);
|
|
}
|
|
|
|
pub fn set_use_modifier_to_send(&mut self, always_use: bool) {
|
|
self.use_modifier_to_send = Some(always_use);
|
|
}
|
|
|
|
pub fn set_profile(&mut self, profile_id: Arc<str>) {
|
|
self.default_profile = Some(profile_id);
|
|
}
|
|
|
|
pub fn add_favorite_model(&mut self, model: LanguageModelSelection) {
|
|
if !self.favorite_models.contains(&model) {
|
|
self.favorite_models.push(model);
|
|
}
|
|
}
|
|
|
|
pub fn remove_favorite_model(&mut self, model: &LanguageModelSelection) {
|
|
self.favorite_models.retain(|m| m != model);
|
|
}
|
|
|
|
pub fn set_tool_default_mode(&mut self, tool_id: &str, mode: ToolPermissionMode) {
|
|
let tool_permissions = self.tool_permissions.get_or_insert_default();
|
|
let tool_rules = tool_permissions
|
|
.tools
|
|
.entry(Arc::from(tool_id))
|
|
.or_default();
|
|
tool_rules.default_mode = Some(mode);
|
|
}
|
|
|
|
pub fn add_tool_allow_pattern(&mut self, tool_name: &str, pattern: String) {
|
|
let tool_permissions = self.tool_permissions.get_or_insert_default();
|
|
let tool_rules = tool_permissions
|
|
.tools
|
|
.entry(Arc::from(tool_name))
|
|
.or_default();
|
|
let always_allow = tool_rules.always_allow.get_or_insert_default();
|
|
if !always_allow.0.iter().any(|r| r.pattern == pattern) {
|
|
always_allow.0.push(ToolRegexRule {
|
|
pattern,
|
|
case_sensitive: None,
|
|
});
|
|
}
|
|
}
|
|
|
|
pub fn add_tool_deny_pattern(&mut self, tool_name: &str, pattern: String) {
|
|
let tool_permissions = self.tool_permissions.get_or_insert_default();
|
|
let tool_rules = tool_permissions
|
|
.tools
|
|
.entry(Arc::from(tool_name))
|
|
.or_default();
|
|
let always_deny = tool_rules.always_deny.get_or_insert_default();
|
|
if !always_deny.0.iter().any(|r| r.pattern == pattern) {
|
|
always_deny.0.push(ToolRegexRule {
|
|
pattern,
|
|
case_sensitive: None,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
#[with_fallible_options]
|
|
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
|
pub struct AgentProfileContent {
|
|
pub name: Arc<str>,
|
|
#[serde(default)]
|
|
pub tools: IndexMap<Arc<str>, bool>,
|
|
/// Whether all context servers are enabled by default.
|
|
pub enable_all_context_servers: Option<bool>,
|
|
#[serde(default)]
|
|
pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>,
|
|
/// The default language model selected when using this profile.
|
|
pub default_model: Option<LanguageModelSelection>,
|
|
}
|
|
|
|
#[with_fallible_options]
|
|
#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
|
pub struct ContextServerPresetContent {
|
|
pub tools: IndexMap<Arc<str>, bool>,
|
|
}
|
|
|
|
#[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum DefaultAgentView {
|
|
#[default]
|
|
Thread,
|
|
TextThread,
|
|
}
|
|
|
|
#[derive(
|
|
Copy,
|
|
Clone,
|
|
Default,
|
|
Debug,
|
|
Serialize,
|
|
Deserialize,
|
|
JsonSchema,
|
|
MergeFrom,
|
|
PartialEq,
|
|
strum::VariantArray,
|
|
strum::VariantNames,
|
|
)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum NotifyWhenAgentWaiting {
|
|
#[default]
|
|
PrimaryScreen,
|
|
AllScreens,
|
|
Never,
|
|
}
|
|
|
|
#[with_fallible_options]
|
|
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
|
|
pub struct LanguageModelSelection {
|
|
pub provider: LanguageModelProviderSetting,
|
|
pub model: String,
|
|
}
|
|
|
|
#[with_fallible_options]
|
|
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
|
|
pub struct LanguageModelParameters {
|
|
pub provider: Option<LanguageModelProviderSetting>,
|
|
pub model: Option<String>,
|
|
#[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
|
|
pub temperature: Option<f32>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, MergeFrom)]
|
|
pub struct LanguageModelProviderSetting(pub String);
|
|
|
|
impl JsonSchema for LanguageModelProviderSetting {
|
|
fn schema_name() -> Cow<'static, str> {
|
|
"LanguageModelProviderSetting".into()
|
|
}
|
|
|
|
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
|
// list the builtin providers as a subset so that we still auto complete them in the settings
|
|
json_schema!({
|
|
"anyOf": [
|
|
{
|
|
"type": "string",
|
|
"enum": [
|
|
"amazon-bedrock",
|
|
"anthropic",
|
|
"copilot_chat",
|
|
"deepseek",
|
|
"google",
|
|
"lmstudio",
|
|
"mistral",
|
|
"ollama",
|
|
"openai",
|
|
"openrouter",
|
|
"vercel",
|
|
"x_ai",
|
|
"zed.dev"
|
|
]
|
|
},
|
|
{
|
|
"type": "string",
|
|
}
|
|
]
|
|
})
|
|
}
|
|
}
|
|
|
|
impl From<String> for LanguageModelProviderSetting {
|
|
fn from(provider: String) -> Self {
|
|
Self(provider)
|
|
}
|
|
}
|
|
|
|
impl From<&str> for LanguageModelProviderSetting {
|
|
fn from(provider: &str) -> Self {
|
|
Self(provider.to_string())
|
|
}
|
|
}
|
|
|
|
#[with_fallible_options]
|
|
#[derive(Default, PartialEq, Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug)]
|
|
pub struct AllAgentServersSettings {
|
|
pub gemini: Option<BuiltinAgentServerSettings>,
|
|
pub claude: Option<BuiltinAgentServerSettings>,
|
|
pub codex: Option<BuiltinAgentServerSettings>,
|
|
|
|
/// Custom agent servers configured by the user
|
|
#[serde(flatten)]
|
|
pub custom: HashMap<String, CustomAgentServerSettings>,
|
|
}
|
|
|
|
#[with_fallible_options]
|
|
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug, PartialEq)]
|
|
pub struct BuiltinAgentServerSettings {
|
|
/// Absolute path to a binary to be used when launching this agent.
|
|
///
|
|
/// This can be used to run a specific binary without automatic downloads or searching `$PATH`.
|
|
#[serde(rename = "command")]
|
|
pub path: Option<PathBuf>,
|
|
/// If a binary is specified in `command`, it will be passed these arguments.
|
|
pub args: Option<Vec<String>>,
|
|
/// If a binary is specified in `command`, it will be passed these environment variables.
|
|
pub env: Option<HashMap<String, String>>,
|
|
/// Whether to skip searching `$PATH` for an agent server binary when
|
|
/// launching this agent.
|
|
///
|
|
/// This has no effect if a `command` is specified. Otherwise, when this is
|
|
/// `false`, Zed will search `$PATH` for an agent server binary and, if one
|
|
/// is found, use it for threads with this agent. If no agent binary is
|
|
/// found on `$PATH`, Zed will automatically install and use its own binary.
|
|
/// When this is `true`, Zed will not search `$PATH`, and will always use
|
|
/// its own binary.
|
|
///
|
|
/// Default: true
|
|
pub ignore_system_version: Option<bool>,
|
|
/// The default mode to use for this agent.
|
|
///
|
|
/// Note: Not only all agents support modes.
|
|
///
|
|
/// Default: None
|
|
pub default_mode: Option<String>,
|
|
/// The default model to use for this agent.
|
|
///
|
|
/// This should be the model ID as reported by the agent.
|
|
///
|
|
/// Default: None
|
|
pub default_model: Option<String>,
|
|
/// The favorite models for this agent.
|
|
///
|
|
/// These are the model IDs as reported by the agent.
|
|
///
|
|
/// Default: []
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
pub favorite_models: Vec<String>,
|
|
/// Default values for session config options.
|
|
///
|
|
/// This is a map from config option ID to value ID.
|
|
///
|
|
/// Default: {}
|
|
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
|
pub default_config_options: HashMap<String, String>,
|
|
/// Favorited values for session config options.
|
|
///
|
|
/// This is a map from config option ID to a list of favorited value IDs.
|
|
///
|
|
/// Default: {}
|
|
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
|
pub favorite_config_option_values: HashMap<String, Vec<String>>,
|
|
}
|
|
|
|
#[with_fallible_options]
|
|
#[derive(Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug, PartialEq)]
|
|
#[serde(tag = "type", rename_all = "snake_case")]
|
|
pub enum CustomAgentServerSettings {
|
|
Custom {
|
|
#[serde(rename = "command")]
|
|
path: PathBuf,
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
args: Vec<String>,
|
|
/// Default: {}
|
|
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
|
env: HashMap<String, String>,
|
|
/// The default mode to use for this agent.
|
|
///
|
|
/// Note: Not only all agents support modes.
|
|
///
|
|
/// Default: None
|
|
default_mode: Option<String>,
|
|
/// The default model to use for this agent.
|
|
///
|
|
/// This should be the model ID as reported by the agent.
|
|
///
|
|
/// Default: None
|
|
default_model: Option<String>,
|
|
/// The favorite models for this agent.
|
|
///
|
|
/// These are the model IDs as reported by the agent.
|
|
///
|
|
/// Default: []
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
favorite_models: Vec<String>,
|
|
/// Default values for session config options.
|
|
///
|
|
/// This is a map from config option ID to value ID.
|
|
///
|
|
/// Default: {}
|
|
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
|
default_config_options: HashMap<String, String>,
|
|
/// Favorited values for session config options.
|
|
///
|
|
/// This is a map from config option ID to a list of favorited value IDs.
|
|
///
|
|
/// Default: {}
|
|
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
|
favorite_config_option_values: HashMap<String, Vec<String>>,
|
|
},
|
|
Extension {
|
|
/// Additional environment variables to pass to the agent.
|
|
///
|
|
/// Default: {}
|
|
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
|
env: HashMap<String, String>,
|
|
/// The default mode to use for this agent.
|
|
///
|
|
/// Note: Not only all agents support modes.
|
|
///
|
|
/// Default: None
|
|
default_mode: Option<String>,
|
|
/// The default model to use for this agent.
|
|
///
|
|
/// This should be the model ID as reported by the agent.
|
|
///
|
|
/// Default: None
|
|
default_model: Option<String>,
|
|
/// The favorite models for this agent.
|
|
///
|
|
/// These are the model IDs as reported by the agent.
|
|
///
|
|
/// Default: []
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
favorite_models: Vec<String>,
|
|
/// Default values for session config options.
|
|
///
|
|
/// This is a map from config option ID to value ID.
|
|
///
|
|
/// Default: {}
|
|
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
|
default_config_options: HashMap<String, String>,
|
|
/// Favorited values for session config options.
|
|
///
|
|
/// This is a map from config option ID to a list of favorited value IDs.
|
|
///
|
|
/// Default: {}
|
|
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
|
favorite_config_option_values: HashMap<String, Vec<String>>,
|
|
},
|
|
Registry {
|
|
/// Additional environment variables to pass to the agent.
|
|
///
|
|
/// Default: {}
|
|
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
|
env: HashMap<String, String>,
|
|
/// The default mode to use for this agent.
|
|
///
|
|
/// Note: Not only all agents support modes.
|
|
///
|
|
/// Default: None
|
|
default_mode: Option<String>,
|
|
/// The default model to use for this agent.
|
|
///
|
|
/// This should be the model ID as reported by the agent.
|
|
///
|
|
/// Default: None
|
|
default_model: Option<String>,
|
|
/// The favorite models for this agent.
|
|
///
|
|
/// These are the model IDs as reported by the agent.
|
|
///
|
|
/// Default: []
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
favorite_models: Vec<String>,
|
|
/// Default values for session config options.
|
|
///
|
|
/// This is a map from config option ID to value ID.
|
|
///
|
|
/// Default: {}
|
|
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
|
default_config_options: HashMap<String, String>,
|
|
/// Favorited values for session config options.
|
|
///
|
|
/// This is a map from config option ID to a list of favorited value IDs.
|
|
///
|
|
/// Default: {}
|
|
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
|
favorite_config_option_values: HashMap<String, Vec<String>>,
|
|
},
|
|
}
|
|
|
|
#[with_fallible_options]
|
|
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
|
pub struct ToolPermissionsContent {
|
|
/// Per-tool permission rules.
|
|
/// Keys: terminal, edit_file, delete_path, move_path, create_directory,
|
|
/// save_file, fetch, web_search
|
|
#[serde(default)]
|
|
pub tools: HashMap<Arc<str>, ToolRulesContent>,
|
|
}
|
|
|
|
#[with_fallible_options]
|
|
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
|
pub struct ToolRulesContent {
|
|
/// Default mode when no regex rules match.
|
|
/// Default: confirm
|
|
pub default_mode: Option<ToolPermissionMode>,
|
|
|
|
/// Regexes for inputs to auto-approve.
|
|
/// For terminal: matches command. For file tools: matches path. For fetch: matches URL.
|
|
/// Default: []
|
|
pub always_allow: Option<ExtendingVec<ToolRegexRule>>,
|
|
|
|
/// Regexes for inputs to auto-reject.
|
|
/// **SECURITY**: These take precedence over ALL other rules, across ALL settings layers.
|
|
/// Default: []
|
|
pub always_deny: Option<ExtendingVec<ToolRegexRule>>,
|
|
|
|
/// Regexes for inputs that must always prompt.
|
|
/// Takes precedence over always_allow but not always_deny.
|
|
/// Default: []
|
|
pub always_confirm: Option<ExtendingVec<ToolRegexRule>>,
|
|
}
|
|
|
|
#[with_fallible_options]
|
|
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
|
pub struct ToolRegexRule {
|
|
/// The regex pattern to match.
|
|
#[serde(default)]
|
|
pub pattern: String,
|
|
|
|
/// Whether the regex is case-sensitive.
|
|
/// Default: false (case-insensitive)
|
|
pub case_sensitive: Option<bool>,
|
|
}
|
|
|
|
#[derive(
|
|
Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom,
|
|
)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ToolPermissionMode {
|
|
/// Auto-approve without prompting.
|
|
Allow,
|
|
/// Auto-reject with an error.
|
|
Deny,
|
|
/// Always prompt for confirmation (default behavior).
|
|
#[default]
|
|
Confirm,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_set_tool_default_mode_creates_structure() {
|
|
let mut settings = AgentSettingsContent::default();
|
|
assert!(settings.tool_permissions.is_none());
|
|
|
|
settings.set_tool_default_mode("terminal", ToolPermissionMode::Allow);
|
|
|
|
let tool_permissions = settings.tool_permissions.as_ref().unwrap();
|
|
let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
|
|
assert_eq!(terminal_rules.default_mode, Some(ToolPermissionMode::Allow));
|
|
}
|
|
|
|
#[test]
|
|
fn test_set_tool_default_mode_updates_existing() {
|
|
let mut settings = AgentSettingsContent::default();
|
|
|
|
settings.set_tool_default_mode("terminal", ToolPermissionMode::Confirm);
|
|
settings.set_tool_default_mode("terminal", ToolPermissionMode::Allow);
|
|
|
|
let tool_permissions = settings.tool_permissions.as_ref().unwrap();
|
|
let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
|
|
assert_eq!(terminal_rules.default_mode, Some(ToolPermissionMode::Allow));
|
|
}
|
|
|
|
#[test]
|
|
fn test_set_tool_default_mode_for_mcp_tool() {
|
|
let mut settings = AgentSettingsContent::default();
|
|
|
|
settings.set_tool_default_mode("mcp:github:create_issue", ToolPermissionMode::Allow);
|
|
|
|
let tool_permissions = settings.tool_permissions.as_ref().unwrap();
|
|
let mcp_rules = tool_permissions
|
|
.tools
|
|
.get("mcp:github:create_issue")
|
|
.unwrap();
|
|
assert_eq!(mcp_rules.default_mode, Some(ToolPermissionMode::Allow));
|
|
}
|
|
|
|
#[test]
|
|
fn test_add_tool_allow_pattern_creates_structure() {
|
|
let mut settings = AgentSettingsContent::default();
|
|
assert!(settings.tool_permissions.is_none());
|
|
|
|
settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
|
|
|
|
let tool_permissions = settings.tool_permissions.as_ref().unwrap();
|
|
let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
|
|
let always_allow = terminal_rules.always_allow.as_ref().unwrap();
|
|
assert_eq!(always_allow.0.len(), 1);
|
|
assert_eq!(always_allow.0[0].pattern, "^cargo\\s");
|
|
}
|
|
|
|
#[test]
|
|
fn test_add_tool_allow_pattern_appends_to_existing() {
|
|
let mut settings = AgentSettingsContent::default();
|
|
|
|
settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
|
|
settings.add_tool_allow_pattern("terminal", "^npm\\s".to_string());
|
|
|
|
let tool_permissions = settings.tool_permissions.as_ref().unwrap();
|
|
let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
|
|
let always_allow = terminal_rules.always_allow.as_ref().unwrap();
|
|
assert_eq!(always_allow.0.len(), 2);
|
|
assert_eq!(always_allow.0[0].pattern, "^cargo\\s");
|
|
assert_eq!(always_allow.0[1].pattern, "^npm\\s");
|
|
}
|
|
|
|
#[test]
|
|
fn test_add_tool_allow_pattern_does_not_duplicate() {
|
|
let mut settings = AgentSettingsContent::default();
|
|
|
|
settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
|
|
settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
|
|
settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
|
|
|
|
let tool_permissions = settings.tool_permissions.as_ref().unwrap();
|
|
let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
|
|
let always_allow = terminal_rules.always_allow.as_ref().unwrap();
|
|
assert_eq!(
|
|
always_allow.0.len(),
|
|
1,
|
|
"Duplicate patterns should not be added"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_add_tool_allow_pattern_for_different_tools() {
|
|
let mut settings = AgentSettingsContent::default();
|
|
|
|
settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
|
|
settings.add_tool_allow_pattern("fetch", "^https?://github\\.com".to_string());
|
|
|
|
let tool_permissions = settings.tool_permissions.as_ref().unwrap();
|
|
|
|
let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
|
|
assert_eq!(
|
|
terminal_rules.always_allow.as_ref().unwrap().0[0].pattern,
|
|
"^cargo\\s"
|
|
);
|
|
|
|
let fetch_rules = tool_permissions.tools.get("fetch").unwrap();
|
|
assert_eq!(
|
|
fetch_rules.always_allow.as_ref().unwrap().0[0].pattern,
|
|
"^https?://github\\.com"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_add_tool_deny_pattern_creates_structure() {
|
|
let mut settings = AgentSettingsContent::default();
|
|
assert!(settings.tool_permissions.is_none());
|
|
|
|
settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
|
|
|
|
let tool_permissions = settings.tool_permissions.as_ref().unwrap();
|
|
let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
|
|
let always_deny = terminal_rules.always_deny.as_ref().unwrap();
|
|
assert_eq!(always_deny.0.len(), 1);
|
|
assert_eq!(always_deny.0[0].pattern, "^rm\\s");
|
|
}
|
|
|
|
#[test]
|
|
fn test_add_tool_deny_pattern_appends_to_existing() {
|
|
let mut settings = AgentSettingsContent::default();
|
|
|
|
settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
|
|
settings.add_tool_deny_pattern("terminal", "^sudo\\s".to_string());
|
|
|
|
let tool_permissions = settings.tool_permissions.as_ref().unwrap();
|
|
let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
|
|
let always_deny = terminal_rules.always_deny.as_ref().unwrap();
|
|
assert_eq!(always_deny.0.len(), 2);
|
|
assert_eq!(always_deny.0[0].pattern, "^rm\\s");
|
|
assert_eq!(always_deny.0[1].pattern, "^sudo\\s");
|
|
}
|
|
|
|
#[test]
|
|
fn test_add_tool_deny_pattern_does_not_duplicate() {
|
|
let mut settings = AgentSettingsContent::default();
|
|
|
|
settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
|
|
settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
|
|
settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
|
|
|
|
let tool_permissions = settings.tool_permissions.as_ref().unwrap();
|
|
let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
|
|
let always_deny = terminal_rules.always_deny.as_ref().unwrap();
|
|
assert_eq!(
|
|
always_deny.0.len(),
|
|
1,
|
|
"Duplicate patterns should not be added"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_add_tool_deny_and_allow_patterns_separate() {
|
|
let mut settings = AgentSettingsContent::default();
|
|
|
|
settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
|
|
settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
|
|
|
|
let tool_permissions = settings.tool_permissions.as_ref().unwrap();
|
|
let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
|
|
|
|
let always_allow = terminal_rules.always_allow.as_ref().unwrap();
|
|
assert_eq!(always_allow.0.len(), 1);
|
|
assert_eq!(always_allow.0[0].pattern, "^cargo\\s");
|
|
|
|
let always_deny = terminal_rules.always_deny.as_ref().unwrap();
|
|
assert_eq!(always_deny.0.len(), 1);
|
|
assert_eq!(always_deny.0[0].pattern, "^rm\\s");
|
|
}
|
|
}
|