Add profile permission overrides and orchestration provider selection

This commit is contained in:
2026-10-05 15:48:01 -05:00
parent c5297066ef
commit 679306a103
24 changed files with 392 additions and 54 deletions
@@ -21,6 +21,7 @@ pub enum PermissionMode {
#[default]
AskFirst,
GalaxyDecides,
AllowAll,
#[serde(other)]
ReadOnly,
}
@@ -31,13 +32,15 @@ impl PermissionMode {
Self::ReadOnly => "Read Only",
Self::AskFirst => "Ask First",
Self::GalaxyDecides => "Galaxy Decides",
Self::AllowAll => "Allow All",
}
}
pub fn description(self) -> &'static str {
match self {
Self::AllowAll => "Run tools without approval or model review. Explicit denylists and workspace restrictions still apply.",
Self::ReadOnly => {
"Only file reads, code searches, grep, and file discovery are available. Commands, edits, MCP, computer use, and agents are disabled."
"Only file reads, code searches, grep, and file discovery are available. Commands, edits, MCP, computer use, and agents are disabled, except explicitly allowlisted shell commands."
}
Self::AskFirst => {
"Ask before taking action. Approved commands are remembered for this profile and working directory; changed commands require approval again."
@@ -53,6 +56,7 @@ impl PermissionMode {
Self::ReadOnly => ActionPermission::Unknown,
Self::AskFirst => ActionPermission::AlwaysAsk,
Self::GalaxyDecides => ActionPermission::GalaxyDecides,
Self::AllowAll => ActionPermission::AlwaysAllow,
}
}
}
@@ -418,6 +422,8 @@ pub struct AIExecutionProfile {
pub permission_mode: PermissionMode,
/// Digests of exact approved commands and their execution environment.
pub approved_command_hashes: Vec<String>,
/// None includes all providers; an empty list explicitly permits none.
pub orchestration_providers: Option<Vec<String>>,
pub is_default_profile: bool,
// Legacy transport projection; permission_mode is authoritative.
#[serde(skip_serializing)]
@@ -445,9 +451,7 @@ pub struct AIExecutionProfile {
/// Always ask for permission for these commands
pub command_denylist: Vec<AgentModeCommandExecutionPredicate>,
/// When the execute_commands is set to AlwaysAsk, autoexecute these commands
// Legacy transport projection; permission_mode is authoritative.
#[serde(skip_serializing)]
/// Explicit command approvals that apply in every permission mode. Denylists win.
pub command_allowlist: Vec<AgentModeCommandExecutionPredicate>,
/// When the read_files is set to AlwaysAsk, autoread from these directories
@@ -484,6 +488,7 @@ pub struct AIExecutionProfile {
struct PersistedExecutionProfile {
permission_mode: Option<PermissionMode>,
approved_command_hashes: Vec<String>,
orchestration_providers: Option<Vec<String>>,
name: String,
is_default_profile: bool,
apply_code_diffs: ActionPermission,
@@ -514,6 +519,7 @@ impl Default for PersistedExecutionProfile {
Self {
permission_mode: None,
approved_command_hashes: Vec::new(),
orchestration_providers: None,
name: profile.name,
is_default_profile: profile.is_default_profile,
apply_code_diffs: profile.apply_code_diffs,
@@ -555,6 +561,7 @@ impl<'de> Deserialize<'de> for AIExecutionProfile {
let mut profile = Self {
permission_mode,
approved_command_hashes: saved.approved_command_hashes,
orchestration_providers: saved.orchestration_providers,
name: saved.name,
is_default_profile: saved.is_default_profile,
apply_code_diffs: saved.apply_code_diffs,
@@ -596,6 +603,7 @@ impl Default for AIExecutionProfile {
is_default_profile: false,
permission_mode: PermissionMode::AskFirst,
approved_command_hashes: Vec::new(),
orchestration_providers: None,
apply_code_diffs: ActionPermission::AlwaysAsk,
read_files: ActionPermission::AlwaysAllow,
execute_commands: ActionPermission::AlwaysAsk,
@@ -621,6 +629,10 @@ impl Default for AIExecutionProfile {
}
impl AIExecutionProfile {
pub fn includes_orchestration_provider(&self, name: &str) -> bool {
self.orchestration_providers.as_ref().is_none_or(|providers| providers.iter().any(|provider| provider == name))
}
/// Adapter for legacy providers that still consume per-tool permission fields.
pub fn project_permission_mode(&mut self) {
let permission = self.permission_mode.action_permission();
@@ -634,24 +646,26 @@ impl AIExecutionProfile {
};
self.write_to_pty = match self.permission_mode {
PermissionMode::GalaxyDecides => WriteToPtyPermission::GalaxyDecides,
PermissionMode::AllowAll => WriteToPtyPermission::AlwaysAllow,
PermissionMode::ReadOnly | PermissionMode::AskFirst => WriteToPtyPermission::AlwaysAsk,
};
self.computer_use = match self.permission_mode {
PermissionMode::ReadOnly => ComputerUsePermission::Never,
PermissionMode::AskFirst => ComputerUsePermission::AlwaysAsk,
PermissionMode::GalaxyDecides => ComputerUsePermission::GalaxyDecides,
PermissionMode::AllowAll => ComputerUsePermission::AlwaysAllow,
};
self.run_agents = match self.permission_mode {
PermissionMode::ReadOnly => RunAgentsPermission::NeverAllow,
PermissionMode::AskFirst => RunAgentsPermission::AlwaysAsk,
PermissionMode::GalaxyDecides => RunAgentsPermission::GalaxyDecides,
PermissionMode::AllowAll => RunAgentsPermission::AlwaysAllow,
};
self.ask_user_question = if self.permission_mode == PermissionMode::AskFirst {
AskUserQuestionPermission::AlwaysAsk
} else {
AskUserQuestionPermission::Never
};
self.command_allowlist.clear();
self.directory_allowlist.clear();
self.mcp_allowlist.clear();
}
@@ -663,6 +677,7 @@ impl AIExecutionProfile {
is_default_profile: false,
permission_mode: PermissionMode::GalaxyDecides,
approved_command_hashes: Vec::new(),
orchestration_providers: None,
apply_code_diffs: ActionPermission::AlwaysAllow,
read_files: ActionPermission::AlwaysAllow,
execute_commands: ActionPermission::AlwaysAllow,
@@ -721,6 +736,7 @@ impl AIExecutionProfile {
is_default_profile: true,
permission_mode: PermissionMode::GalaxyDecides,
approved_command_hashes: Vec::new(),
orchestration_providers: None,
apply_code_diffs: ActionPermission::AlwaysAllow,
read_files: ActionPermission::AlwaysAllow,
execute_commands: ActionPermission::AlwaysAllow,
@@ -45,6 +45,7 @@ fn serialized_profiles_only_store_one_permission_mode_and_bounded_approvals() {
PermissionMode::ReadOnly,
PermissionMode::AskFirst,
PermissionMode::GalaxyDecides,
PermissionMode::AllowAll,
] {
let profile = AIExecutionProfile {
permission_mode: mode,
@@ -60,7 +61,6 @@ fn serialized_profiles_only_store_one_permission_mode_and_bounded_approvals() {
"write_to_pty",
"run_agents",
"computer_use",
"command_allowlist",
"directory_allowlist",
"mcp_allowlist",
] {
@@ -75,3 +75,42 @@ fn serialized_profiles_only_store_one_permission_mode_and_bounded_approvals() {
assert_eq!(restored.approved_command_hashes.first().unwrap(), "44");
}
}
#[test]
fn explicit_command_approvals_and_provider_selection_survive_roundtrip_and_mode_changes() {
let mut profile = AIExecutionProfile {
permission_mode: PermissionMode::AllowAll,
command_allowlist: vec![AgentModeCommandExecutionPredicate::new_regex("^cargo test$").unwrap()],
orchestration_providers: Some(vec!["My LiteLLM".to_string()]),
..Default::default()
};
for mode in [PermissionMode::ReadOnly, PermissionMode::AskFirst, PermissionMode::GalaxyDecides, PermissionMode::AllowAll] {
profile.permission_mode = mode;
profile.project_permission_mode();
let saved = serde_json::to_value(&profile).unwrap();
let restored: AIExecutionProfile = serde_json::from_value(saved).unwrap();
assert_eq!(restored.permission_mode, mode);
assert_eq!(restored.command_allowlist, profile.command_allowlist);
assert_eq!(restored.includes_orchestration_provider("My LiteLLM"), true);
assert_eq!(restored.includes_orchestration_provider("ChatGPT"), false);
}
}
#[test]
fn missing_provider_selection_keeps_existing_profiles_unrestricted() {
let profile: AIExecutionProfile = serde_json::from_str("{}").unwrap();
assert_eq!(profile.includes_orchestration_provider("New provider"), true);
let excluded = AIExecutionProfile { orchestration_providers: Some(Vec::new()), ..Default::default() };
assert_eq!(excluded.includes_orchestration_provider("New provider"), false);
}
#[test]
fn allow_all_projects_every_tool_to_always_allow() {
let mut profile = AIExecutionProfile { permission_mode: PermissionMode::AllowAll, ..Default::default() };
profile.project_permission_mode();
assert_eq!(profile.execute_commands, ActionPermission::AlwaysAllow);
assert_eq!(profile.apply_code_diffs, ActionPermission::AlwaysAllow);
assert_eq!(profile.mcp_permissions, ActionPermission::AlwaysAllow);
assert_eq!(profile.computer_use, ComputerUsePermission::AlwaysAllow);
assert_eq!(profile.run_agents, RunAgentsPermission::AlwaysAllow);
}