feat: use the same permission flow for enable extensions (#2302)

This commit is contained in:
Yingjie He
2025-04-23 08:54:04 -07:00
committed by GitHub
parent 08682507d9
commit cc755100f0
16 changed files with 77 additions and 420 deletions
+2 -4
View File
@@ -3,8 +3,7 @@ use console::style;
use goose::agents::extension::ToolInfo;
use goose::agents::extension_manager::get_parameter_names;
use goose::agents::platform_tools::{
PLATFORM_ENABLE_EXTENSION_TOOL_NAME, PLATFORM_LIST_RESOURCES_TOOL_NAME,
PLATFORM_READ_RESOURCE_TOOL_NAME,
PLATFORM_LIST_RESOURCES_TOOL_NAME, PLATFORM_READ_RESOURCE_TOOL_NAME,
};
use goose::agents::Agent;
use goose::agents::{extension::Envs, ExtensionConfig};
@@ -1015,8 +1014,7 @@ pub async fn configure_tool_permissions_dialog() -> Result<(), Box<dyn Error>> {
.await
.into_iter()
.filter(|tool| {
tool.name != PLATFORM_ENABLE_EXTENSION_TOOL_NAME
&& tool.name != PLATFORM_LIST_RESOURCES_TOOL_NAME
tool.name != PLATFORM_LIST_RESOURCES_TOOL_NAME
&& tool.name != PLATFORM_READ_RESOURCE_TOOL_NAME
})
.map(|tool| {
-24
View File
@@ -17,7 +17,6 @@ use completion::GooseCompleter;
use etcetera::choose_app_strategy;
use etcetera::AppStrategy;
use goose::agents::extension::{Envs, ExtensionConfig};
use goose::agents::platform_tools::PLATFORM_ENABLE_EXTENSION_TOOL_NAME;
use goose::agents::{Agent, SessionConfig};
use goose::config::Config;
use goose::message::{Message, MessageContent};
@@ -623,29 +622,6 @@ impl Session {
principal_type: PrincipalType::Tool,
permission,
},).await;
} else if let Some(MessageContent::ExtensionRequest(enable_extension_request)) = message.content.first() {
output::hide_thinking();
let extension_action = if enable_extension_request.tool_name == PLATFORM_ENABLE_EXTENSION_TOOL_NAME {
"enable"
} else {
"disable"
};
let prompt = format!("Goose would like to {} the following extension, do you approve?", extension_action);
let confirmed = cliclack::select(prompt)
.item(true, "Yes, for this session", format!("{} the extension for this session", extension_action))
.item(false, "No", format!("Do not {} the extension", extension_action))
.interact()?;
let permission = if confirmed {
Permission::AllowOnce
} else {
Permission::DenyOnce
};
self.agent.handle_confirmation(enable_extension_request.id.clone(), PermissionConfirmation {
principal_type: PrincipalType::Extension,
permission,
},).await;
}
// otherwise we have a model/tool to render
else {
+37 -38
View File
@@ -22,8 +22,8 @@ use tracing::{debug, error, instrument, warn};
use crate::agents::extension::{ExtensionConfig, ExtensionResult, ToolInfo};
use crate::agents::extension_manager::{get_parameter_names, ExtensionManager};
use crate::agents::platform_tools::{
PLATFORM_LIST_RESOURCES_TOOL_NAME, PLATFORM_READ_RESOURCE_TOOL_NAME,
PLATFORM_SEARCH_AVAILABLE_EXTENSIONS_TOOL_NAME,
PLATFORM_ENABLE_EXTENSION_TOOL_NAME, PLATFORM_LIST_RESOURCES_TOOL_NAME,
PLATFORM_READ_RESOURCE_TOOL_NAME, PLATFORM_SEARCH_AVAILABLE_EXTENSIONS_TOOL_NAME,
};
use crate::agents::prompt_manager::PromptManager;
use crate::agents::types::SessionConfig;
@@ -33,9 +33,7 @@ use mcp_core::{
};
use super::platform_tools;
use super::tool_execution::{
ExtensionInstallResult, ToolFuture, CHAT_MODE_TOOL_SKIPPED_RESPONSE, DECLINED_RESPONSE,
};
use super::tool_execution::{ToolFuture, CHAT_MODE_TOOL_SKIPPED_RESPONSE, DECLINED_RESPONSE};
const MAX_TRUNCATION_ATTEMPTS: usize = 3;
const ESTIMATE_FACTOR_DECAY: f32 = 0.9;
@@ -114,6 +112,16 @@ impl Agent {
tool_call: mcp_core::tool::ToolCall,
request_id: String,
) -> (String, Result<Vec<Content>, ToolError>) {
if tool_call.name == PLATFORM_ENABLE_EXTENSION_TOOL_NAME {
let extension_name = tool_call
.arguments
.get("extension_name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
return self.enable_extension(extension_name, request_id).await;
}
let extension_manager = self.extension_manager.lock().await;
let result = if tool_call.name == PLATFORM_READ_RESOURCE_TOOL_NAME {
// Check if the tool is read_resource and handle it separately
@@ -418,26 +426,26 @@ impl Agent {
// What remains is handling the remaining tool requests (enable extension,
// regular tool calls) in goose_mode == ["auto", "approve" or "smart_approve"]
let mut permission_manager = PermissionManager::default();
let permission_check_result = check_tool_permissions(&remaining_requests,
&mode,
tools_with_readonly_annotation.clone(),
tools_without_annotation.clone(),
&mut permission_manager,
self.provider()).await;
let (permission_check_result, enable_extension_request_ids) = check_tool_permissions(
&remaining_requests,
&mode,
tools_with_readonly_annotation.clone(),
tools_without_annotation.clone(),
&mut permission_manager,
self.provider(),
).await;
// Handle pre-approved and read-only tools in parallel
let mut tool_futures: Vec<ToolFuture> = Vec::new();
let mut install_results: Vec<ExtensionInstallResult> = Vec::new();
let install_results_arc = Arc::new(Mutex::new(install_results));
// Skip the confirmation for approved tools
for request in &permission_check_result.approved {
if let Ok(tool_call) = request.tool_call.clone() {
let tool_future = self.dispatch_tool_call(tool_call, request.id.clone());
tool_futures.push(Box::pin(tool_future));
tool_futures.push(Box::pin(tool_future));
}
}
for request in &permission_check_result.denied {
let mut response = message_tool_response.lock().await;
*response = response.clone().with_tool_response(
@@ -446,51 +454,42 @@ impl Agent {
);
}
// we need interior mutability in handle_approval_tool_requests
// We need interior mutability in handle_approval_tool_requests
let tool_futures_arc = Arc::new(Mutex::new(tool_futures));
// Process tools requiring approval (enable extension, regular tool calls)
let mut tool_approval_stream = self.handle_approval_tool_requests(
&permission_check_result.needs_approval,
install_results_arc.clone(),
tool_futures_arc.clone(),
&mut permission_manager,
message_tool_response.clone()
message_tool_response.clone(),
);
// we have a stream of tool_approval_requests to handle
// execution is yeield back to this reply loop, and is of the same Message
// type, so we can yield the Message back up to be handled and grab and
// We have a stream of tool_approval_requests to handle
// Execution is yielded back to this reply loop, and is of the same Message
// type, so we can yield the Message back up to be handled and grab any
// confirmations or denials
while let Some(msg) = tool_approval_stream.try_next().await? {
yield msg;
}
tool_futures = {
// Lock the mutex asynchronously.
// Lock the mutex asynchronously
let mut futures_lock = tool_futures_arc.lock().await;
// Drain the vector and collect into a new Vec.
// Drain the vector and collect into a new Vec
futures_lock.drain(..).collect::<Vec<_>>()
};
install_results = {
// Lock the mutex asynchronously.
let mut results_lock = install_results_arc.lock().await;
// Drain the vector and collect into a new Vec.
results_lock.drain(..).collect::<Vec<_>>()
};
// Wait for all tool calls to complete
let results = futures::future::join_all(tool_futures).await;
let mut all_install_successful = true;
// Check if any install results had errors before processing them
let all_install_successful = !install_results.iter().any(|(_, result)| result.is_err());
for (request_id, output) in results.into_iter().chain(install_results.into_iter()) {
for (request_id, output) in results.into_iter() {
if enable_extension_request_ids.contains(&request_id) && output.is_err(){
all_install_successful = false;
}
let mut response = message_tool_response.lock().await;
*response = response.clone().with_tool_response(
request_id,
output
);
*response = response.clone().with_tool_response(request_id, output);
}
// Update system prompt and tools if installations were successful
+11 -24
View File
@@ -10,8 +10,6 @@ use tokio::sync::Mutex;
use crate::config::permission::PermissionLevel;
use crate::config::PermissionManager;
use crate::message::{Message, ToolRequest};
use crate::permission::permission_confirmation::PrincipalType;
use crate::permission::permission_judge::get_confirmation_message;
use crate::permission::Permission;
use mcp_core::{Content, ToolError};
@@ -19,9 +17,6 @@ use mcp_core::{Content, ToolError};
pub(crate) type ToolFuture<'a> =
Pin<Box<dyn Future<Output = (String, Result<Vec<Content>, ToolError>)> + Send + 'a>>;
pub(crate) type ToolFuturesVec<'a> = Arc<Mutex<Vec<ToolFuture<'a>>>>;
// Type alias for extension installation results
pub(crate) type ExtensionInstallResult = (String, Result<Vec<Content>, ToolError>);
pub(crate) type ExtensionInstallResults = Arc<Mutex<Vec<ExtensionInstallResult>>>;
use crate::agents::Agent;
@@ -42,7 +37,6 @@ impl Agent {
pub(crate) fn handle_approval_tool_requests<'a>(
&'a self,
tool_requests: &'a [ToolRequest],
install_results: ExtensionInstallResults,
tool_futures: ToolFuturesVec<'a>,
permission_manager: &'a mut PermissionManager,
message_tool_response: Arc<Mutex<Message>>,
@@ -50,31 +44,24 @@ impl Agent {
try_stream! {
for request in tool_requests {
if let Ok(tool_call) = request.tool_call.clone() {
let (principal_type, confirmation) = get_confirmation_message(&request.id.clone(), tool_call.clone());
let confirmation = Message::user().with_tool_confirmation_request(
request.id.clone(),
tool_call.name.clone(),
tool_call.arguments.clone(),
Some("Goose would like to call the above tool. Allow? (y/n):".to_string()),
);
yield confirmation;
let mut rx = self.confirmation_rx.lock().await;
while let Some((req_id, confirmation)) = rx.recv().await {
if req_id == request.id {
if confirmation.permission == Permission::AllowOnce || confirmation.permission == Permission::AlwaysAllow {
if principal_type == PrincipalType::Extension {
let extension_name = tool_call.arguments.get("extension_name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let tool_future = self.dispatch_tool_call(tool_call.clone(), request.id.clone());
let mut futures = tool_futures.lock().await;
futures.push(Box::pin(tool_future));
let mut results = install_results.lock().await;
let install_result = self.enable_extension(extension_name, request.id.clone()).await;
results.push(install_result);
} else {
// Add this tool call to the futures collection
let tool_future = self.dispatch_tool_call(tool_call.clone(), request.id.clone());
let mut futures = tool_futures.lock().await;
futures.push(Box::pin(tool_future));
if confirmation.permission == Permission::AlwaysAllow {
permission_manager.update_user_permission(&tool_call.name, PermissionLevel::AlwaysAllow);
}
if confirmation.permission == Permission::AlwaysAllow {
permission_manager.update_user_permission(&tool_call.name, PermissionLevel::AlwaysAllow);
}
} else {
// User declined - add declined response
-42
View File
@@ -59,14 +59,6 @@ pub struct ToolConfirmationRequest {
pub prompt: Option<String>,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExtensionRequest {
pub id: String,
pub extension_name: String,
pub tool_name: String,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ThinkingContent {
pub thinking: String,
@@ -95,7 +87,6 @@ pub enum MessageContent {
ToolRequest(ToolRequest),
ToolResponse(ToolResponse),
ToolConfirmationRequest(ToolConfirmationRequest),
ExtensionRequest(ExtensionRequest),
FrontendToolRequest(FrontendToolRequest),
Thinking(ThinkingContent),
RedactedThinking(RedactedThinkingContent),
@@ -145,18 +136,6 @@ impl MessageContent {
})
}
pub fn extension_request<S: Into<String>>(
id: S,
extension_name: String,
tool_name: String,
) -> Self {
MessageContent::ExtensionRequest(ExtensionRequest {
id: id.into(),
extension_name,
tool_name,
})
}
pub fn thinking<S1: Into<String>, S2: Into<String>>(thinking: S1, signature: S2) -> Self {
MessageContent::Thinking(ThinkingContent {
thinking: thinking.into(),
@@ -198,14 +177,6 @@ impl MessageContent {
}
}
pub fn as_extension_request(&self) -> Option<&ExtensionRequest> {
if let MessageContent::ExtensionRequest(ref extension_request) = self {
Some(extension_request)
} else {
None
}
}
pub fn as_tool_response_text(&self) -> Option<String> {
if let Some(tool_response) = self.as_tool_response() {
if let Ok(contents) = &tool_response.tool_result {
@@ -365,19 +336,6 @@ impl Message {
))
}
pub fn with_extension_request<S: Into<String>>(
self,
id: S,
extension_name: String,
tool_name: String,
) -> Self {
self.with_content(MessageContent::extension_request(
id,
extension_name,
tool_name,
))
}
pub fn with_frontend_tool_request<S: Into<String>>(
self,
id: S,
+19 -59
View File
@@ -6,14 +6,11 @@ use crate::providers::base::Provider;
use chrono::Utc;
use indoc::indoc;
use mcp_core::tool::ToolAnnotations;
use mcp_core::ToolCall;
use mcp_core::{tool::Tool, TextContent};
use serde_json::{json, Value};
use std::collections::HashSet;
use std::sync::Arc;
use super::permission_confirmation::PrincipalType;
/// Creates the tool definition for checking read-only permissions.
fn create_read_only_tool() -> Tool {
Tool::new(
@@ -154,36 +151,6 @@ pub async fn detect_read_only_tools(
}
}
/// Gets the boolean value whether the message is enable extension related and
/// the cconfirmation message based on the tool call
pub fn get_confirmation_message(request_id: &str, tool_call: ToolCall) -> (PrincipalType, Message) {
if tool_call.name == PLATFORM_ENABLE_EXTENSION_TOOL_NAME {
(
PrincipalType::Extension,
Message::user().with_extension_request(
request_id,
tool_call
.arguments
.get("extension_name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
tool_call.name.clone(),
),
)
} else {
(
PrincipalType::Tool,
Message::user().with_tool_confirmation_request(
request_id,
tool_call.name.clone(),
tool_call.arguments.clone(),
Some("Goose would like to call the above tool. Allow? (y/n):".to_string()),
),
)
}
}
// Define return structure
pub struct PermissionCheckResult {
pub approved: Vec<ToolRequest>,
@@ -198,26 +165,24 @@ pub async fn check_tool_permissions(
tools_without_annotation: HashSet<String>,
permission_manager: &mut PermissionManager,
provider: Arc<dyn Provider>,
) -> PermissionCheckResult {
) -> (PermissionCheckResult, Vec<String>) {
let mut approved = vec![];
let mut needs_approval = vec![];
let mut denied = vec![];
let mut llm_detect_candidates = vec![];
let mut enable_extension_request_ids = vec![];
for request in candidate_requests {
if let Ok(tool_call) = request.tool_call.clone() {
// Always ask approval for enable extension tool.
if tool_call.name == PLATFORM_ENABLE_EXTENSION_TOOL_NAME {
// Insert at the front of the list so that enable extension can be run before other tools.
needs_approval.insert(0, request.clone());
continue;
}
if mode == "chat" {
continue;
} else if mode == "auto" {
approved.push(request.clone());
} else {
if tool_call.name == PLATFORM_ENABLE_EXTENSION_TOOL_NAME {
enable_extension_request_ids.push(request.id.clone());
}
// 1. Check user-defined permission
if let Some(level) = permission_manager.get_user_permission(&tool_call.name) {
match level {
@@ -284,11 +249,14 @@ pub async fn check_tool_permissions(
}
}
PermissionCheckResult {
approved,
needs_approval,
denied,
}
(
PermissionCheckResult {
approved,
needs_approval,
denied,
},
enable_extension_request_ids,
)
}
#[cfg(test)]
@@ -471,7 +439,7 @@ mod tests {
vec![tool_request_1, tool_request_2, enable_extension];
// Call the function under test
let result = check_tool_permissions(
let (result, enable_extension_request_ids) = check_tool_permissions(
&candidate_requests,
"smart_approve",
tools_with_readonly_annotation,
@@ -485,21 +453,13 @@ mod tests {
assert_eq!(result.approved.len(), 1); // file_reader should be approved
assert_eq!(result.needs_approval.len(), 2); // data_fetcher should need approval
assert_eq!(result.denied.len(), 0); // No tool should be denied in this test
assert_eq!(enable_extension_request_ids.len(), 1);
// Ensure the right tools are in the approved and needs_approval lists
assert!(result.approved.iter().any(|req| req.id == "tool_1"));
assert!(result.needs_approval.iter().any(|req| req.id == "tool_2"));
let tool_0 = result.needs_approval.get(0);
assert!(
tool_0.is_some(),
"Expected at least one tool in needs_approval"
);
assert_eq!(
tool_0.unwrap().id,
"tool_3",
"PLATFORM_ENABLE_EXTENSION_TOOL_NAME should be the first in needs_approval"
);
assert!(result.needs_approval.iter().any(|req| req.id == "tool_3"));
assert!(enable_extension_request_ids.iter().any(|id| id == "tool_3"));
}
#[tokio::test]
@@ -538,7 +498,7 @@ mod tests {
let candidate_requests: Vec<ToolRequest> = vec![tool_request_1, tool_request_2];
// Call the function under test
let result = check_tool_permissions(
let (result, _) = check_tool_permissions(
&candidate_requests,
"auto",
tools_with_readonly_annotation,
@@ -60,9 +60,6 @@ pub fn format_messages(messages: &[Message]) -> Vec<Value> {
MessageContent::ToolConfirmationRequest(_tool_confirmation_request) => {
// Skip tool confirmation requests
}
MessageContent::ExtensionRequest(_extension_request) => {
// Skip extension requests
}
MessageContent::Thinking(thinking) => {
content.push(json!({
"type": "thinking",
@@ -31,9 +31,6 @@ pub fn to_bedrock_message_content(content: &MessageContent) -> Result<bedrock::C
MessageContent::ToolConfirmationRequest(_tool_confirmation_request) => {
bedrock::ContentBlock::Text("".to_string())
}
MessageContent::ExtensionRequest(_extension_request) => {
bedrock::ContentBlock::Text("".to_string())
}
MessageContent::Image(_) => {
bail!("Image content is not supported by Bedrock provider yet")
}
@@ -179,9 +179,6 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<
MessageContent::ToolConfirmationRequest(_) => {
// Skip tool confirmation requests
}
MessageContent::ExtensionRequest(_) => {
// Skip enable extension requests
}
MessageContent::Image(image) => {
// Handle direct image content
content_array.push(json!({
@@ -147,9 +147,6 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<
MessageContent::ToolConfirmationRequest(_) => {
// Skip tool confirmation requests
}
MessageContent::ExtensionRequest(_) => {
// Skip enable extension requests
}
MessageContent::Image(image) => {
// Handle direct image content
converted["content"] = json!([convert_image(image, image_format)]);