diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 68b6bf03ab..716975bd17 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -852,6 +852,7 @@ pub struct ToolPermissionContext { pub enum ToolPermissionScope { ToolInput, SymlinkTarget, + AgentSkills, } impl ToolPermissionContext { @@ -871,6 +872,11 @@ impl ToolPermissionContext { } } + pub fn for_agent_skills(mut self) -> Self { + self.scope = ToolPermissionScope::AgentSkills; + self + } + /// Builds the permission options for this tool context. /// /// This is the canonical source for permission option generation. @@ -916,6 +922,22 @@ impl ToolPermissionContext { ]); } + // Skills always prompt, so offer only once-only allow/deny. + if self.scope == ToolPermissionScope::AgentSkills { + return acp_thread::PermissionOptions::Flat(vec![ + acp::PermissionOption::new( + acp::PermissionOptionId::new("allow"), + "Allow", + acp::PermissionOptionKind::AllowOnce, + ), + acp::PermissionOption::new( + acp::PermissionOptionId::new("deny"), + "Deny", + acp::PermissionOptionKind::RejectOnce, + ), + ]); + } + // Check if the user's shell supports POSIX-like command chaining. // See the doc comment above for the full explanation of why this is needed. let shell_supports_always_allow = if tool_name == TerminalTool::NAME { diff --git a/crates/agent/src/tools/copy_path_tool.rs b/crates/agent/src/tools/copy_path_tool.rs index 6d300551a5..42a7311c57 100644 --- a/crates/agent/src/tools/copy_path_tool.rs +++ b/crates/agent/src/tools/copy_path_tool.rs @@ -328,6 +328,13 @@ mod tests { title.contains("agent skills"), "Authorization title should mention agent skills, got: {title}", ); + assert!( + auth.options + .first_option_of_kind(acp::PermissionOptionKind::AllowAlways) + .is_none(), + "agent skills prompt must not offer an \"Always allow\" option: {:?}", + auth.options, + ); auth.response .send(acp_thread::SelectedPermissionOutcome::new( acp::PermissionOptionId::new("allow"), @@ -387,6 +394,13 @@ mod tests { title.contains("agent skills"), "Authorization title should mention agent skills, got: {title}", ); + assert!( + auth.options + .first_option_of_kind(acp::PermissionOptionKind::AllowAlways) + .is_none(), + "agent skills prompt must not offer an \"Always allow\" option: {:?}", + auth.options, + ); auth.response .send(acp_thread::SelectedPermissionOutcome::new( acp::PermissionOptionId::new("allow"), diff --git a/crates/agent/src/tools/create_directory_tool.rs b/crates/agent/src/tools/create_directory_tool.rs index dcd051c2a7..fdec1da2b7 100644 --- a/crates/agent/src/tools/create_directory_tool.rs +++ b/crates/agent/src/tools/create_directory_tool.rs @@ -243,6 +243,13 @@ mod tests { title.contains("agent skills"), "Authorization title should mention agent skills, got: {title}", ); + assert!( + auth.options + .first_option_of_kind(acp::PermissionOptionKind::AllowAlways) + .is_none(), + "agent skills prompt must not offer an \"Always allow\" option: {:?}", + auth.options, + ); auth.response .send(acp_thread::SelectedPermissionOutcome::new( acp::PermissionOptionId::new("allow"), diff --git a/crates/agent/src/tools/delete_path_tool.rs b/crates/agent/src/tools/delete_path_tool.rs index e791e6feb5..bae19d7692 100644 --- a/crates/agent/src/tools/delete_path_tool.rs +++ b/crates/agent/src/tools/delete_path_tool.rs @@ -329,6 +329,13 @@ mod tests { title.contains("agent skills"), "Authorization title should mention agent skills, got: {title}", ); + assert!( + auth.options + .first_option_of_kind(acp::PermissionOptionKind::AllowAlways) + .is_none(), + "agent skills prompt must not offer an \"Always allow\" option: {:?}", + auth.options, + ); auth.response .send(acp_thread::SelectedPermissionOutcome::new( acp::PermissionOptionId::new("allow"), diff --git a/crates/agent/src/tools/edit_file_tool.rs b/crates/agent/src/tools/edit_file_tool.rs index 2801c8878d..e80e2110a1 100644 --- a/crates/agent/src/tools/edit_file_tool.rs +++ b/crates/agent/src/tools/edit_file_tool.rs @@ -1320,6 +1320,20 @@ mod tests { event.tool_call.fields.title, Some("Edit `root/.agents/skills/my-skill/SKILL.md` (agent skills)".into()) ); + // Skills always prompt, so no "Always allow" option is offered. + assert!( + event + .options + .first_option_of_kind(acp::PermissionOptionKind::AllowAlways) + .is_none(), + "agent skills prompt must not offer an \"Always allow\" option: {:?}", + event.options, + ); + assert!( + matches!(event.options, acp_thread::PermissionOptions::Flat(_)), + "agent skills prompt should use flat allow/deny options: {:?}", + event.options, + ); // 5.6: The global .agents/skills directory is sensitive — still prompts let global_skill_path = agent_skills::global_skills_dir() diff --git a/crates/agent/src/tools/move_path_tool.rs b/crates/agent/src/tools/move_path_tool.rs index 000f17a38c..8156d0714a 100644 --- a/crates/agent/src/tools/move_path_tool.rs +++ b/crates/agent/src/tools/move_path_tool.rs @@ -354,6 +354,13 @@ mod tests { title.contains("agent skills"), "Authorization title should mention agent skills, got: {title}", ); + assert!( + auth.options + .first_option_of_kind(acp::PermissionOptionKind::AllowAlways) + .is_none(), + "agent skills prompt must not offer an \"Always allow\" option: {:?}", + auth.options, + ); auth.response .send(acp_thread::SelectedPermissionOutcome::new( acp::PermissionOptionId::new("allow"), @@ -413,6 +420,13 @@ mod tests { title.contains("agent skills"), "Authorization title should mention agent skills, got: {title}", ); + assert!( + auth.options + .first_option_of_kind(acp::PermissionOptionKind::AllowAlways) + .is_none(), + "agent skills prompt must not offer an \"Always allow\" option: {:?}", + auth.options, + ); auth.response .send(acp_thread::SelectedPermissionOutcome::new( acp::PermissionOptionId::new("allow"), diff --git a/crates/agent/src/tools/tool_permissions.rs b/crates/agent/src/tools/tool_permissions.rs index 7dd0972f0a..5fb1ade6ab 100644 --- a/crates/agent/src/tools/tool_permissions.rs +++ b/crates/agent/src/tools/tool_permissions.rs @@ -509,9 +509,11 @@ pub fn authorize_with_sensitive_settings( Some(SensitiveSettingsKind::Global) => { event_stream.authorize_always_prompt(format!("{title} (settings)"), context, cx) } - Some(SensitiveSettingsKind::AgentSkills) => { - event_stream.authorize_always_prompt(format!("{title} (agent skills)"), context, cx) - } + Some(SensitiveSettingsKind::AgentSkills) => event_stream.authorize_always_prompt( + format!("{title} (agent skills)"), + context.for_agent_skills(), + cx, + ), None => event_stream.authorize(title, context, cx), } } @@ -761,7 +763,8 @@ pub fn authorize_file_edit( let context = ToolPermissionContext::new( &tool_name, vec![path_owned.to_string_lossy().to_string()], - ); + ) + .for_agent_skills(); event_stream.authorize_always_prompt( format!("{title} (agent skills)"), context, diff --git a/crates/agent/src/tools/write_file_tool.rs b/crates/agent/src/tools/write_file_tool.rs index 735a9d23a9..af9c857f63 100644 --- a/crates/agent/src/tools/write_file_tool.rs +++ b/crates/agent/src/tools/write_file_tool.rs @@ -373,6 +373,13 @@ mod tests { title.contains("agent skills"), "Authorization title should mention agent skills, got: {title}", ); + assert!( + auth.options + .first_option_of_kind(acp::PermissionOptionKind::AllowAlways) + .is_none(), + "agent skills prompt must not offer an \"Always allow\" option: {:?}", + auth.options, + ); auth.response .send(acp_thread::SelectedPermissionOutcome::new( acp::PermissionOptionId::new("allow"),