feat(agent): event-driven tool consent handshake (explicit UI prompts)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
use crate::config::{Config, McpResourceConfig, McpServerConfig};
|
||||
use crate::consent::ConsentManager;
|
||||
use crate::consent::{ConsentManager, ConsentScope};
|
||||
use crate::conversation::ConversationManager;
|
||||
use crate::credentials::CredentialManager;
|
||||
use crate::encryption::{self, VaultHandle};
|
||||
@@ -34,6 +34,7 @@ use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::sync::Mutex as TokioMutex;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub enum SessionOutcome {
|
||||
@@ -44,6 +45,36 @@ pub enum SessionOutcome {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ControllerEvent {
|
||||
ToolRequested {
|
||||
request_id: Uuid,
|
||||
message_id: Uuid,
|
||||
tool_name: String,
|
||||
data_types: Vec<String>,
|
||||
endpoints: Vec<String>,
|
||||
tool_calls: Vec<ToolCall>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct PendingToolRequest {
|
||||
message_id: Uuid,
|
||||
tool_name: String,
|
||||
data_types: Vec<String>,
|
||||
endpoints: Vec<String>,
|
||||
tool_calls: Vec<ToolCall>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ToolConsentResolution {
|
||||
pub request_id: Uuid,
|
||||
pub message_id: Uuid,
|
||||
pub tool_name: String,
|
||||
pub scope: ConsentScope,
|
||||
pub tool_calls: Vec<ToolCall>,
|
||||
}
|
||||
|
||||
fn extract_resource_content(value: &Value) -> Option<String> {
|
||||
match value {
|
||||
Value::Null => Some(String::new()),
|
||||
@@ -111,6 +142,8 @@ pub struct SessionController {
|
||||
enable_code_tools: bool,
|
||||
current_mode: Mode,
|
||||
missing_oauth_servers: Vec<String>,
|
||||
event_tx: Option<UnboundedSender<ControllerEvent>>,
|
||||
pending_tool_requests: HashMap<Uuid, PendingToolRequest>,
|
||||
}
|
||||
|
||||
async fn build_tools(
|
||||
@@ -331,6 +364,7 @@ impl SessionController {
|
||||
storage: Arc<StorageManager>,
|
||||
ui: Arc<dyn UiController>,
|
||||
enable_code_tools: bool,
|
||||
event_tx: Option<UnboundedSender<ControllerEvent>>,
|
||||
) -> Result<Self> {
|
||||
let config_arc = Arc::new(TokioMutex::new(config));
|
||||
// Acquire the config asynchronously to avoid blocking the runtime.
|
||||
@@ -435,6 +469,8 @@ impl SessionController {
|
||||
enable_code_tools,
|
||||
current_mode: initial_mode,
|
||||
missing_oauth_servers,
|
||||
event_tx,
|
||||
pending_tool_requests: HashMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1222,14 +1258,84 @@ impl SessionController {
|
||||
.append_stream_chunk(message_id, &chunk.message.content, chunk.is_final)
|
||||
}
|
||||
|
||||
pub fn check_streaming_tool_calls(&self, message_id: Uuid) -> Option<Vec<ToolCall>> {
|
||||
self.conversation
|
||||
pub fn check_streaming_tool_calls(&mut self, message_id: Uuid) -> Option<Vec<ToolCall>> {
|
||||
let maybe_calls = self
|
||||
.conversation
|
||||
.active()
|
||||
.messages
|
||||
.iter()
|
||||
.find(|m| m.id == message_id)
|
||||
.and_then(|m| m.tool_calls.clone())
|
||||
.filter(|calls| !calls.is_empty())
|
||||
.filter(|calls| !calls.is_empty());
|
||||
|
||||
let calls = maybe_calls?;
|
||||
|
||||
if !self
|
||||
.pending_tool_requests
|
||||
.values()
|
||||
.any(|pending| pending.message_id == message_id)
|
||||
{
|
||||
if let Some((tool_name, data_types, endpoints)) =
|
||||
self.check_tools_consent_needed(&calls).into_iter().next()
|
||||
{
|
||||
let request_id = Uuid::new_v4();
|
||||
let pending = PendingToolRequest {
|
||||
message_id,
|
||||
tool_name: tool_name.clone(),
|
||||
data_types: data_types.clone(),
|
||||
endpoints: endpoints.clone(),
|
||||
tool_calls: calls.clone(),
|
||||
};
|
||||
self.pending_tool_requests.insert(request_id, pending);
|
||||
|
||||
if let Some(tx) = &self.event_tx {
|
||||
let _ = tx.send(ControllerEvent::ToolRequested {
|
||||
request_id,
|
||||
message_id,
|
||||
tool_name,
|
||||
data_types,
|
||||
endpoints,
|
||||
tool_calls: calls.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(calls)
|
||||
}
|
||||
|
||||
pub fn resolve_tool_consent(
|
||||
&mut self,
|
||||
request_id: Uuid,
|
||||
scope: ConsentScope,
|
||||
) -> Result<ToolConsentResolution> {
|
||||
let pending = self
|
||||
.pending_tool_requests
|
||||
.remove(&request_id)
|
||||
.ok_or_else(|| {
|
||||
Error::InvalidInput(format!("Unknown tool consent request: {}", request_id))
|
||||
})?;
|
||||
|
||||
let PendingToolRequest {
|
||||
message_id,
|
||||
tool_name,
|
||||
data_types,
|
||||
endpoints,
|
||||
tool_calls,
|
||||
..
|
||||
} = pending;
|
||||
|
||||
if !matches!(scope, ConsentScope::Denied) {
|
||||
self.grant_consent_with_scope(&tool_name, data_types, endpoints, scope.clone());
|
||||
}
|
||||
|
||||
Ok(ToolConsentResolution {
|
||||
request_id,
|
||||
message_id,
|
||||
tool_name,
|
||||
scope,
|
||||
tool_calls,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cancel_stream(&mut self, message_id: Uuid, notice: &str) -> Result<()> {
|
||||
@@ -1352,7 +1458,7 @@ mod tests {
|
||||
let provider: Arc<dyn Provider> = Arc::new(MockProvider::default()) as Arc<dyn Provider>;
|
||||
let ui = Arc::new(NoOpUiController);
|
||||
|
||||
let session = SessionController::new(provider, config, storage, ui, false)
|
||||
let session = SessionController::new(provider, config, storage, ui, false, None)
|
||||
.await
|
||||
.expect("session");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user