199 lines
8.6 KiB
Rust
199 lines
8.6 KiB
Rust
use std::collections::HashMap;
|
|
use std::future::Future;
|
|
use std::sync::Arc;
|
|
|
|
use async_stream::try_stream;
|
|
use futures::stream::{self, BoxStream};
|
|
use futures::{Stream, StreamExt};
|
|
use tokio::sync::Mutex;
|
|
use tokio_util::sync::CancellationToken;
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use crate::config::permission::PermissionLevel;
|
|
use crate::mcp_utils::ToolResult;
|
|
use crate::permission::Permission;
|
|
use rmcp::model::{Content, ServerNotification};
|
|
|
|
/// Context passed through the tool call dispatch chain.
|
|
pub struct ToolCallContext {
|
|
pub session_id: String,
|
|
pub working_dir: Option<PathBuf>,
|
|
pub tool_call_request_id: Option<String>,
|
|
}
|
|
|
|
impl ToolCallContext {
|
|
pub fn new(
|
|
session_id: String,
|
|
working_dir: Option<PathBuf>,
|
|
tool_call_request_id: Option<String>,
|
|
) -> Self {
|
|
Self {
|
|
session_id,
|
|
working_dir,
|
|
tool_call_request_id,
|
|
}
|
|
}
|
|
|
|
pub fn working_dir_str(&self) -> Option<&str> {
|
|
self.working_dir.as_ref().and_then(|p| p.to_str())
|
|
}
|
|
}
|
|
|
|
// ToolCallResult combines the result of a tool call with an optional notification stream that
|
|
// can be used to receive notifications from the tool.
|
|
pub struct ToolCallResult {
|
|
pub result: Box<dyn Future<Output = ToolResult<rmcp::model::CallToolResult>> + Send + Unpin>,
|
|
pub notification_stream: Option<Box<dyn Stream<Item = ServerNotification> + Send + Unpin>>,
|
|
}
|
|
|
|
impl From<ToolResult<rmcp::model::CallToolResult>> for ToolCallResult {
|
|
fn from(result: ToolResult<rmcp::model::CallToolResult>) -> Self {
|
|
Self {
|
|
result: Box::new(futures::future::ready(result)),
|
|
notification_stream: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
use super::agent::{tool_stream, ToolStream};
|
|
use crate::agents::Agent;
|
|
use crate::conversation::message::{Message, ToolRequest};
|
|
use crate::session::Session;
|
|
use crate::tool_inspection::get_security_finding_id_from_results;
|
|
|
|
pub const DECLINED_RESPONSE: &str = "The user has declined to run this tool. \
|
|
DO NOT attempt to call this tool again. \
|
|
If there are no alternative methods to proceed, clearly explain the situation and STOP.";
|
|
|
|
pub const CHAT_MODE_TOOL_SKIPPED_RESPONSE: &str = "Let the user know the tool call was skipped in goose chat mode. \
|
|
DO NOT apologize for skipping the tool call. DO NOT say sorry. \
|
|
Provide an explanation of what the tool call would do, structured as a \
|
|
plan for the user. Again, DO NOT apologize. \
|
|
**Example Plan:**\n \
|
|
1. **Identify Task Scope** - Determine the purpose and expected outcome.\n \
|
|
2. **Outline Steps** - Break down the steps.\n \
|
|
If needed, adjust the explanation based on user preferences or questions.";
|
|
|
|
impl Agent {
|
|
pub(crate) fn handle_approval_tool_requests<'a>(
|
|
&'a self,
|
|
tool_requests: &'a [ToolRequest],
|
|
tool_futures: Arc<Mutex<Vec<(String, ToolStream)>>>,
|
|
request_to_response_map: &'a HashMap<String, Arc<Mutex<Message>>>,
|
|
cancellation_token: Option<CancellationToken>,
|
|
session: &'a Session,
|
|
inspection_results: &'a [crate::tool_inspection::InspectionResult],
|
|
) -> BoxStream<'a, anyhow::Result<Message>> {
|
|
try_stream! {
|
|
for request in tool_requests.iter() {
|
|
if let Ok(tool_call) = request.tool_call.clone() {
|
|
// Find the corresponding inspection result for this tool request
|
|
let security_message = inspection_results.iter()
|
|
.find(|result| result.tool_request_id == request.id)
|
|
.and_then(|result| {
|
|
if let crate::tool_inspection::InspectionAction::RequireApproval(Some(message)) = &result.action {
|
|
Some(message.clone())
|
|
} else {
|
|
None
|
|
}
|
|
});
|
|
|
|
let confirmation_rx = self.tool_confirmation_router.register(request.id.clone()).await;
|
|
|
|
let action_required_msg = Message::assistant()
|
|
.with_action_required(
|
|
request.id.clone(),
|
|
tool_call.name.to_string().clone(),
|
|
tool_call.arguments.clone().unwrap_or_default(),
|
|
security_message,
|
|
)
|
|
.user_only();
|
|
yield action_required_msg;
|
|
|
|
let confirmation = confirmation_rx.await
|
|
.map_err(|_| anyhow::anyhow!("Confirmation channel closed for request {}", request.id))?;
|
|
|
|
// Log user decision if this was a security alert
|
|
if let Some(finding_id) = get_security_finding_id_from_results(&request.id, inspection_results) {
|
|
tracing::info!(
|
|
monotonic_counter.goose.prompt_injection_user_decisions = 1,
|
|
decision = ?confirmation.permission,
|
|
finding_id = %finding_id,
|
|
tool_request_id = %request.id,
|
|
"Prompt injection detection: user decision on command injection finding"
|
|
);
|
|
}
|
|
|
|
if confirmation.permission == Permission::AllowOnce || confirmation.permission == Permission::AlwaysAllow {
|
|
let (req_id, tool_result) = self.dispatch_tool_call(tool_call.clone(), request.id.clone(), cancellation_token.clone(), session).await;
|
|
let mut futures = tool_futures.lock().await;
|
|
|
|
futures.push((req_id, match tool_result {
|
|
Ok(result) => tool_stream(
|
|
result.notification_stream.unwrap_or_else(|| Box::new(stream::empty())),
|
|
result.result,
|
|
),
|
|
Err(e) => tool_stream(
|
|
Box::new(stream::empty()),
|
|
futures::future::ready(Err(e)),
|
|
),
|
|
}));
|
|
|
|
// Update the shared permission manager when user selects "Always Allow"
|
|
if confirmation.permission == Permission::AlwaysAllow {
|
|
self.tool_inspection_manager
|
|
.update_permission_manager(&tool_call.name, PermissionLevel::AlwaysAllow)
|
|
.await;
|
|
}
|
|
} else {
|
|
// User declined - update the specific response message for this request
|
|
if let Some(response_msg) = request_to_response_map.get(&request.id) {
|
|
let mut response = response_msg.lock().await;
|
|
*response = response.clone().with_tool_response_with_metadata(
|
|
request.id.clone(),
|
|
Ok(rmcp::model::CallToolResult::error(vec![Content::text(DECLINED_RESPONSE)])),
|
|
request.metadata.as_ref(),
|
|
);
|
|
}
|
|
|
|
if confirmation.permission == Permission::AlwaysDeny {
|
|
self.tool_inspection_manager
|
|
.update_permission_manager(&tool_call.name, PermissionLevel::NeverAllow)
|
|
.await;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}.boxed()
|
|
}
|
|
|
|
pub(crate) fn handle_frontend_tool_request<'a>(
|
|
&'a self,
|
|
tool_request: &'a ToolRequest,
|
|
message_tool_response: Arc<Mutex<Message>>,
|
|
) -> BoxStream<'a, anyhow::Result<Message>> {
|
|
try_stream! {
|
|
if let Ok(tool_call) = tool_request.tool_call.clone() {
|
|
if self.is_frontend_tool(&tool_call.name).await {
|
|
// Send frontend tool request and wait for response
|
|
yield Message::assistant().with_frontend_tool_request(
|
|
tool_request.id.clone(),
|
|
Ok(tool_call.clone())
|
|
);
|
|
|
|
if let Some((id, result)) = self.tool_result_rx.lock().await.recv().await {
|
|
let mut response = message_tool_response.lock().await;
|
|
*response = response.clone().with_tool_response_with_metadata(
|
|
id,
|
|
result,
|
|
tool_request.metadata.as_ref(),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.boxed()
|
|
}
|
|
}
|