use std::path::PathBuf; use ai::LLMId; use cloud_objects::cloud_object::{ GenericCloudObject, GenericServerObject, GenericStringModel, JsonObjectType, }; use cloud_objects::ids::GenericStringObjectId; use lazy_static::lazy_static; use regex::Regex; use serde::{Deserialize, Serialize}; use warp_core::channel::ChannelState; use warp_core::features::FeatureFlag; use crate::{JsonModel, JsonSerializer}; pub const PROFILE_NAME_MAX_LENGTH: usize = 50; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum ActionPermission { AlwaysAllow, AlwaysAsk, /// Independently assess each action before allowing execution. #[serde(alias = "AgentDecides")] GalaxyDecides, // This is intended to catch deserialization errors whenever we add new variants to this enum. Say we // want to add a "Never" variant. Without this catch-all, old clients wouldn't be able to deserialize // a "Never" into one of the existing options. #[serde(other)] Unknown, } impl ActionPermission { pub fn description(&self) -> &'static str { match self { ActionPermission::GalaxyDecides => { "Galaxy reviews each action with your terminal-use model. Risk 0–3 runs automatically; risk 4–5 asks you, and is rejected after 60 seconds without a response." } ActionPermission::Unknown => "Unknown setting.", ActionPermission::AlwaysAllow => { "Give the Agent full autonomy — no manual approval ever required." } ActionPermission::AlwaysAsk => { "Require explicit approval before the Agent takes any action." } } } pub fn is_always_ask(&self) -> bool { matches!(self, Self::AlwaysAsk) } pub fn is_always_allow(&self) -> bool { matches!(self, Self::AlwaysAllow) } } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum WriteToPtyPermission { // This is for backwards compatibility with the old "Never" value. #[serde(alias = "Never")] AlwaysAllow, #[default] AlwaysAsk, AskOnFirstWrite, /// Independently assess each action before allowing execution. GalaxyDecides, // This is intended to catch deserialization errors whenever we add new variants to this enum. #[serde(other)] Unknown, } impl WriteToPtyPermission { pub fn description(&self) -> &'static str { match self { WriteToPtyPermission::GalaxyDecides => { "Galaxy reviews each action with your terminal-use model. Risk 0–3 runs automatically; risk 4–5 asks you, and is rejected after 60 seconds without a response." } WriteToPtyPermission::AlwaysAllow => ActionPermission::AlwaysAllow.description(), WriteToPtyPermission::AskOnFirstWrite => { "The agent will ask for permission the first time it needs to interact with a running command. After that, it will continue automatically for the rest of that command." } WriteToPtyPermission::AlwaysAsk => { "The agent will always ask for permission to interact with a running command." } WriteToPtyPermission::Unknown => ActionPermission::Unknown.description(), } } pub fn is_always_allow(&self) -> bool { matches!(self, Self::AlwaysAllow) } } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum ComputerUsePermission { #[default] Never, AlwaysAsk, AlwaysAllow, /// Independently assess each action before allowing execution. GalaxyDecides, // This is intended to catch deserialization errors whenever we add new variants to this enum. #[serde(other)] Unknown, } impl ComputerUsePermission { pub fn description(&self) -> &'static str { match self { ComputerUsePermission::GalaxyDecides => { "Galaxy reviews each action with your terminal-use model. Risk 0–3 runs automatically; risk 4–5 asks you, and is rejected after 60 seconds without a response." } ComputerUsePermission::Never => { "Computer use tools are disabled and will not be available to the Agent." } ComputerUsePermission::AlwaysAsk => { "Require explicit approval before the Agent uses computer use tools." } ComputerUsePermission::AlwaysAllow => { "Give the Agent full autonomy to use computer use tools without approval." } ComputerUsePermission::Unknown => "Unknown setting.", } } pub fn is_enabled(&self) -> bool { !matches!(self, Self::Never | Self::Unknown) } pub fn is_always_allow(&self) -> bool { matches!(self, Self::AlwaysAllow) } } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum RunAgentsPermission { NeverAllow, AlwaysAllow, #[default] AlwaysAsk, /// Independently assess each action before allowing execution. GalaxyDecides, // This is intended to catch deserialization errors whenever we add new variants to this enum. #[serde(other)] Unknown, } impl RunAgentsPermission { pub fn description(&self) -> &'static str { match self { RunAgentsPermission::GalaxyDecides => { "Galaxy reviews each action with your terminal-use model. Risk 0–3 runs automatically; risk 4–5 asks you, and is rejected after 60 seconds without a response." } RunAgentsPermission::NeverAllow => { "The Agent cannot run child agents and the run_agents tool will not be available." } RunAgentsPermission::AlwaysAllow => { "Give the Agent full autonomy to run child agents without approval." } RunAgentsPermission::AlwaysAsk => { "Require explicit approval before the Agent runs child agents." } RunAgentsPermission::Unknown => "Unknown setting.", } } pub fn is_enabled(&self) -> bool { matches!( self, Self::AlwaysAllow | Self::AlwaysAsk | Self::GalaxyDecides ) } pub fn is_always_allow(&self) -> bool { matches!(self, Self::AlwaysAllow) } pub fn is_never_allow(&self) -> bool { matches!(self, Self::NeverAllow | Self::Unknown) } } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum AskUserQuestionPermission { /// Never pause; skip questions and continue with best judgment. Never, /// Pause and wait for the user, unless auto-approve mode is enabled. AskExceptInAutoApprove, /// Always pause and wait for the user to answer before continuing, even in auto-approve mode. #[default] AlwaysAsk, // This is intended to catch deserialization errors whenever we add new variants to this enum. #[serde(other)] Unknown, } impl AskUserQuestionPermission { pub fn label(&self) -> &'static str { match self { AskUserQuestionPermission::Never => "Never ask", AskUserQuestionPermission::AskExceptInAutoApprove => "Ask unless auto-approve", AskUserQuestionPermission::AlwaysAsk | AskUserQuestionPermission::Unknown => { "Always ask" } } } pub fn description(&self) -> &'static str { match self { AskUserQuestionPermission::AskExceptInAutoApprove | AskUserQuestionPermission::Unknown => { "The Agent may ask a question and pause for your response, but will continue automatically when auto-approve is on." } AskUserQuestionPermission::Never => { "The Agent will not ask questions and will continue with its best judgment." } AskUserQuestionPermission::AlwaysAsk => { "The Agent may ask a question and will pause for your response even when auto-approve is on." } } } } /// Predicate types to match commands that can be executed by Agent Mode. #[derive(Debug, Serialize, Deserialize, Clone)] enum AgentModeCommandExecutionPredicateType { /// A regex with start (`^`) and end (`$`) anchors. /// /// We want regex rules to apply to the entire cmd string so we anchor them /// (there isn't any efficient way to apply to the entire cmd string at match-time). #[serde(with = "serde_regex")] AnchoredRegex(Regex), } impl AgentModeCommandExecutionPredicateType { fn new_regex(regex: &str) -> Result { // Redundant anchors aren't a problem so we can unconditionally add them. let anchored_regex = Regex::new(&format!("^{regex}$"))?; Ok(Self::AnchoredRegex(anchored_regex)) } fn matches(&self, cmd: &str) -> bool { match self { Self::AnchoredRegex(regex) => regex.is_match(cmd), } } } impl PartialEq for AgentModeCommandExecutionPredicateType { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::AnchoredRegex(a), Self::AnchoredRegex(b)) => { // Indexing should be safe since they're guaranteed to have at least // the anchors around them. let a_unanchored = &a.as_str()[1..a.as_str().len() - 1]; let b_unanchored = &b.as_str()[1..b.as_str().len() - 1]; a_unanchored == b_unanchored } } } } impl std::fmt::Display for AgentModeCommandExecutionPredicateType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::AnchoredRegex(regex) => { write!(f, "{}", ®ex.as_str()[1..regex.as_str().len() - 1]) } } } } /// A wrapper around [`AgentModeCommandExecutionPredicateType`] to enforce /// the use of the provided constructors rather than direct construction of the variants. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(transparent)] pub struct AgentModeCommandExecutionPredicate(AgentModeCommandExecutionPredicateType); impl schemars::JsonSchema for AgentModeCommandExecutionPredicate { fn schema_name() -> std::borrow::Cow<'static, str> { std::borrow::Cow::Borrowed("AgentModeCommandExecutionPredicate") } fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { generator.subschema_for::() } } impl AgentModeCommandExecutionPredicate { pub fn new_regex(regex: &str) -> Result { Ok(Self(AgentModeCommandExecutionPredicateType::new_regex( regex, )?)) } pub fn matches(&self, cmd: &str) -> bool { self.0.matches(cmd) } } impl std::fmt::Display for AgentModeCommandExecutionPredicate { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } impl settings_value::SettingsValue for AgentModeCommandExecutionPredicate { fn to_file_value(&self) -> serde_json::Value { serde_json::Value::String(self.to_string()) } fn from_file_value(value: &serde_json::Value) -> Option { value.as_str().and_then(|s| Self::new_regex(s).ok()) } } lazy_static! { static ref OPTIONAL_ARGS_REGEX: Regex = Regex::new(r"(\s.*)?").expect("Can parse optional args regex"); } cfg_if::cfg_if! { if #[cfg(test)] { lazy_static! { // Compiling the regexes for the default command execution allowlist/denylist can be slow // in an unoptimized build, so we use empty lists in unit tests. pub static ref DEFAULT_COMMAND_EXECUTION_ALLOWLIST: Vec = vec![]; pub static ref DEFAULT_COMMAND_EXECUTION_DENYLIST: Vec = vec![]; } } else { lazy_static! { pub static ref DEFAULT_COMMAND_EXECUTION_ALLOWLIST: Vec = vec![ AgentModeCommandExecutionPredicate::new_regex(&format!("cat{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default cat rule into regex"), AgentModeCommandExecutionPredicate::new_regex(&format!("echo{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default echo rule into regex"), AgentModeCommandExecutionPredicate::new_regex("find .*").expect("Can parse default find rule into regex"), AgentModeCommandExecutionPredicate::new_regex(&format!("grep{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default grep rule into regex"), AgentModeCommandExecutionPredicate::new_regex(&format!("ls{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default ls rule into regex"), AgentModeCommandExecutionPredicate::new_regex("which .*").expect("Can parse default which rule into regex"), ]; pub static ref DEFAULT_COMMAND_EXECUTION_DENYLIST: Vec = vec![ AgentModeCommandExecutionPredicate::new_regex(&format!("bash{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default bash rule into regex"), AgentModeCommandExecutionPredicate::new_regex(&format!("fish{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default fish rule into regex"), AgentModeCommandExecutionPredicate::new_regex(&format!("pwsh{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default pwsh rule into regex"), AgentModeCommandExecutionPredicate::new_regex(&format!("sh{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default sh rule into regex"), AgentModeCommandExecutionPredicate::new_regex(&format!("zsh{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default zsh rule into regex"), AgentModeCommandExecutionPredicate::new_regex(&format!("curl{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default curl rule into regex"), AgentModeCommandExecutionPredicate::new_regex(&format!("eval{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default eval rule into regex"), AgentModeCommandExecutionPredicate::new_regex(&format!("exec{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default exec rule into regex"), AgentModeCommandExecutionPredicate::new_regex(&format!("source{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default source rule into regex"), AgentModeCommandExecutionPredicate::new_regex(&format!("wget{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default wget rule into regex"), AgentModeCommandExecutionPredicate::new_regex(&format!("dig{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default dig rule into regex"), AgentModeCommandExecutionPredicate::new_regex(&format!("nslookup{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default nslookup rule into regex"), AgentModeCommandExecutionPredicate::new_regex(&format!("host{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default host rule into regex"), AgentModeCommandExecutionPredicate::new_regex(&format!("ssh{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default ssh rule into regex"), AgentModeCommandExecutionPredicate::new_regex(&format!("scp{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default scp rule into regex"), AgentModeCommandExecutionPredicate::new_regex(&format!("rsync{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default rsync rule into regex"), AgentModeCommandExecutionPredicate::new_regex(&format!("telnet{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default telnet rule into regex"), AgentModeCommandExecutionPredicate::new_regex(&format!("rm{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default rm rule into regex"), ]; } } } /// Core data structure representing an AI execution profile, which includes model configuration, /// behavior settings, and permissions. /// /// NOTE: `planning_model` was removed after planning via subagent was deprecated; serialized legacy /// profiles may include a `planning_model` field and this field name should remain reserved /// indefinitely. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(default)] pub struct AIExecutionProfile { pub name: String, pub is_default_profile: bool, pub apply_code_diffs: ActionPermission, pub read_files: ActionPermission, pub execute_commands: ActionPermission, pub write_to_pty: WriteToPtyPermission, pub mcp_permissions: ActionPermission, pub ask_user_question: AskUserQuestionPermission, pub run_agents: RunAgentsPermission, /// Always ask for permission for these commands pub command_denylist: Vec, /// When the execute_commands is set to AlwaysAsk, autoexecute these commands pub command_allowlist: Vec, /// When the read_files is set to AlwaysAsk, autoread from these directories pub directory_allowlist: Vec, pub mcp_allowlist: Vec, pub mcp_denylist: Vec, pub computer_use: ComputerUsePermission, pub base_model: Option, pub coding_model: Option, pub cli_agent_model: Option, pub computer_use_model: Option, pub context_window_limit: Option, /// Whether plans created by the agent should be automatically synced to Warp Drive pub autosync_plans_to_warp_drive: bool, /// Whether the agent may use web search when helpful for completing tasks pub web_search_enabled: bool, } impl Default for AIExecutionProfile { fn default() -> Self { Self { name: Default::default(), is_default_profile: false, apply_code_diffs: ActionPermission::GalaxyDecides, read_files: ActionPermission::GalaxyDecides, execute_commands: ActionPermission::AlwaysAsk, write_to_pty: WriteToPtyPermission::AlwaysAsk, mcp_permissions: ActionPermission::GalaxyDecides, ask_user_question: AskUserQuestionPermission::AlwaysAsk, run_agents: RunAgentsPermission::AlwaysAsk, command_denylist: DEFAULT_COMMAND_EXECUTION_DENYLIST.clone(), command_allowlist: Vec::new(), directory_allowlist: Vec::new(), mcp_allowlist: Vec::new(), mcp_denylist: Vec::new(), computer_use: ComputerUsePermission::Never, base_model: None, coding_model: None, cli_agent_model: None, computer_use_model: None, context_window_limit: None, autosync_plans_to_warp_drive: true, web_search_enabled: true, } } } impl AIExecutionProfile { #[cfg(feature = "agent_mode_evals")] pub fn create_agent_mode_eval_profile() -> Self { Self { name: "Agent Mode Eval".to_string(), is_default_profile: false, apply_code_diffs: ActionPermission::AlwaysAllow, read_files: ActionPermission::AlwaysAllow, execute_commands: ActionPermission::AlwaysAllow, write_to_pty: WriteToPtyPermission::AlwaysAllow, mcp_permissions: ActionPermission::AlwaysAllow, ask_user_question: AskUserQuestionPermission::Never, run_agents: RunAgentsPermission::AlwaysAllow, command_denylist: Vec::new(), command_allowlist: Vec::new(), directory_allowlist: Vec::new(), mcp_allowlist: Vec::new(), mcp_denylist: Vec::new(), computer_use: ComputerUsePermission::Never, base_model: None, coding_model: None, cli_agent_model: None, computer_use_model: None, context_window_limit: None, autosync_plans_to_warp_drive: false, web_search_enabled: true, } } /// This creates a CLI-specific profile that will never ask the user for permission, /// since we cannot do so in a non-interactive setting. pub fn create_default_cli_profile( is_sandboxed: bool, computer_use_override: Option, ) -> Self { let command_denylist = if is_sandboxed { Vec::new() } else { DEFAULT_COMMAND_EXECUTION_DENYLIST.to_vec() }; let computer_use_permission = match computer_use_override { Some(true) => { if is_sandboxed || FeatureFlag::LocalComputerUse.is_enabled() { ComputerUsePermission::AlwaysAllow } else { ComputerUsePermission::Never } } Some(false) => ComputerUsePermission::Never, None => { if is_sandboxed && ChannelState::channel().is_dogfood() { ComputerUsePermission::AlwaysAllow } else { ComputerUsePermission::Never } } }; Self { name: "Default (CLI)".to_owned(), is_default_profile: true, apply_code_diffs: ActionPermission::AlwaysAllow, read_files: ActionPermission::AlwaysAllow, execute_commands: ActionPermission::AlwaysAllow, mcp_permissions: ActionPermission::AlwaysAllow, write_to_pty: WriteToPtyPermission::AlwaysAllow, ask_user_question: AskUserQuestionPermission::Never, run_agents: RunAgentsPermission::AlwaysAllow, command_denylist, command_allowlist: DEFAULT_COMMAND_EXECUTION_ALLOWLIST.to_vec(), directory_allowlist: Vec::new(), mcp_allowlist: Vec::new(), mcp_denylist: Vec::new(), computer_use: computer_use_permission, base_model: None, coding_model: None, cli_agent_model: None, computer_use_model: None, context_window_limit: None, autosync_plans_to_warp_drive: FeatureFlag::SyncAmbientPlans.is_enabled(), web_search_enabled: true, } } } impl JsonModel for AIExecutionProfile { fn json_object_type() -> JsonObjectType { JsonObjectType::AIExecutionProfile } } pub type CloudAIExecutionProfile = GenericCloudObject; pub type CloudAIExecutionProfileModel = GenericStringModel; pub type ServerAIExecutionProfile = GenericServerObject;