mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
WIP
This commit is contained in:
Generated
+1
@@ -2597,6 +2597,7 @@ dependencies = [
|
||||
"axum 0.8.1",
|
||||
"base64 0.21.7",
|
||||
"blake3",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"criterion",
|
||||
"ctor",
|
||||
|
||||
@@ -34,6 +34,7 @@ pub async fn run() -> Result<()> {
|
||||
.allow_headers(Any);
|
||||
|
||||
let app = crate::routes::configure(app_state)
|
||||
.await
|
||||
.layer(middleware::from_fn_with_state(
|
||||
secret_key.clone(),
|
||||
check_token,
|
||||
|
||||
@@ -7,36 +7,18 @@ use axum::{
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use futures::Stream;
|
||||
use goose::agents::approval::{ApprovalAction, ApprovalType};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use goose::agents::approval::ApprovalState;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
convert::Infallible,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use tokio::sync::{mpsc, oneshot, RwLock};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// A request for user approval
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ApprovalRequest {
|
||||
pub request_id: String,
|
||||
pub session_id: String,
|
||||
#[serde(flatten)]
|
||||
pub approval_type: ApprovalType,
|
||||
}
|
||||
|
||||
/// A response to an approval request
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ApprovalResponse {
|
||||
pub request_id: String,
|
||||
pub action: ApprovalAction,
|
||||
}
|
||||
// Re-export for OpenAPI
|
||||
pub use goose::agents::approval::{ApprovalRequest, ApprovalResponse};
|
||||
|
||||
pub struct SseResponse {
|
||||
rx: ReceiverStream<String>,
|
||||
@@ -48,6 +30,7 @@ impl SseResponse {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(alexhancock) - Dedupe into chat stream
|
||||
impl Stream for SseResponse {
|
||||
type Item = Result<Bytes, Infallible>;
|
||||
|
||||
@@ -72,95 +55,6 @@ impl IntoResponse for SseResponse {
|
||||
}
|
||||
}
|
||||
|
||||
/// State for managing approval requests and responses
|
||||
#[derive(Clone)]
|
||||
pub struct ApprovalState {
|
||||
/// Channel for broadcasting approval requests to connected UI clients
|
||||
broadcast_tx: Arc<RwLock<Vec<mpsc::Sender<String>>>>,
|
||||
/// Pending approval requests awaiting user response
|
||||
pending_requests: Arc<RwLock<HashMap<String, oneshot::Sender<ApprovalAction>>>>,
|
||||
}
|
||||
|
||||
impl ApprovalState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
broadcast_tx: Arc::new(RwLock::new(Vec::new())),
|
||||
pending_requests: Arc::new(RwLock::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Submit a response to an approval request
|
||||
pub async fn submit_response(
|
||||
&self,
|
||||
request_id: String,
|
||||
action: ApprovalAction,
|
||||
) -> Result<(), String> {
|
||||
let mut pending = self.pending_requests.write().await;
|
||||
if let Some(tx) = pending.remove(&request_id) {
|
||||
tx.send(action)
|
||||
.map_err(|_| "Failed to send response".to_string())?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Request not found or already responded".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a new SSE client connection
|
||||
async fn add_client(&self, tx: mpsc::Sender<String>) {
|
||||
let mut senders = self.broadcast_tx.write().await;
|
||||
senders.push(tx);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ApprovalState {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement the ApprovalHandler trait from goose crate
|
||||
#[async_trait::async_trait]
|
||||
impl goose::agents::approval::ApprovalHandler for ApprovalState {
|
||||
async fn request_approval(
|
||||
&self,
|
||||
session_id: String,
|
||||
approval_type: ApprovalType,
|
||||
) -> Result<ApprovalAction, String> {
|
||||
let request_id = Uuid::new_v4().to_string();
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
// store: request id -> channel so the channel can be used to continue the task when requests come back in
|
||||
let mut pending = self.pending_requests.write().await;
|
||||
pending.insert(request_id.clone(), tx);
|
||||
|
||||
let request = ApprovalRequest {
|
||||
request_id: request_id.clone(),
|
||||
session_id,
|
||||
approval_type,
|
||||
};
|
||||
|
||||
// Send the approval request to the user
|
||||
let message = serde_json::to_string(&request).map_err(|e| e.to_string())?;
|
||||
let sse_message = format!("data: {}\n\n", message);
|
||||
let senders = self.broadcast_tx.read().await;
|
||||
for sender in senders.iter() {
|
||||
let _ = sender.send(sse_message.clone()).await;
|
||||
}
|
||||
|
||||
// timeout and expire state in 5 minutes if user has not responded
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(300), rx).await {
|
||||
Ok(Ok(action)) => Ok(action),
|
||||
Ok(Err(_)) => Err("Response channel closed".to_string()),
|
||||
Err(_) => {
|
||||
// Timeout - clean up pending request
|
||||
let mut pending = self.pending_requests.write().await;
|
||||
pending.remove(&request_id);
|
||||
Err("Approval request timed out".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/approval",
|
||||
|
||||
@@ -18,7 +18,10 @@ use std::sync::Arc;
|
||||
use axum::Router;
|
||||
|
||||
// Function to configure all routes
|
||||
pub fn configure(state: Arc<crate::state::AppState>) -> Router {
|
||||
pub async fn configure(state: Arc<crate::state::AppState>) -> Router {
|
||||
// Get the global approval state for the approval routes
|
||||
let approval_state = goose::agents::approval::ApprovalState::global().await;
|
||||
|
||||
Router::new()
|
||||
.merge(health::routes())
|
||||
.merge(reply::routes(state.clone()))
|
||||
@@ -31,5 +34,5 @@ pub fn configure(state: Arc<crate::state::AppState>) -> Router {
|
||||
.merge(session::routes(state.clone()))
|
||||
.merge(schedule::routes(state.clone()))
|
||||
.merge(setup::routes(state.clone()))
|
||||
.merge(approval::routes(state.approval_state.clone()))
|
||||
.merge(approval::routes(approval_state))
|
||||
}
|
||||
|
||||
@@ -7,12 +7,9 @@ use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::routes::approval::ApprovalState;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub(crate) agent_manager: Arc<AgentManager>,
|
||||
pub approval_state: Arc<ApprovalState>,
|
||||
pub recipe_file_hash_map: Arc<Mutex<HashMap<String, PathBuf>>>,
|
||||
pub session_counter: Arc<AtomicUsize>,
|
||||
/// Tracks sessions that have already emitted recipe telemetry to prevent double counting.
|
||||
@@ -22,18 +19,12 @@ pub struct AppState {
|
||||
impl AppState {
|
||||
pub async fn new() -> anyhow::Result<Arc<AppState>> {
|
||||
let agent_manager = AgentManager::instance().await?;
|
||||
let approval_state = Arc::new(ApprovalState::new());
|
||||
|
||||
agent_manager
|
||||
.set_approval_handler(approval_state.clone())
|
||||
.await;
|
||||
|
||||
Ok(Arc::new(Self {
|
||||
agent_manager,
|
||||
recipe_file_hash_map: Arc::new(Mutex::new(HashMap::new())),
|
||||
session_counter: Arc::new(AtomicUsize::new(0)),
|
||||
recipe_session_tracker: Arc::new(Mutex::new(HashSet::new())),
|
||||
approval_state,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@ unicode-normalization = "0.1"
|
||||
oauth2 = "5.0.0"
|
||||
schemars = { version = "1.0.4", default-features = false, features = ["derive"] }
|
||||
insta = "1.43.2"
|
||||
bytes = "1.10.1"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
winapi = { version = "0.3", features = ["wincred"] }
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tokio::sync::{mpsc, oneshot, OnceCell, RwLock};
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
static GLOBAL_APPROVAL_STATE: OnceCell<Arc<ApprovalState>> = OnceCell::const_new();
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
@@ -51,3 +56,115 @@ pub trait ApprovalHandler: Send + Sync {
|
||||
approval_type: ApprovalType,
|
||||
) -> Result<ApprovalAction, String>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ApprovalRequest {
|
||||
pub request_id: String,
|
||||
pub session_id: String,
|
||||
#[serde(flatten)]
|
||||
pub approval_type: ApprovalType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ApprovalResponse {
|
||||
pub request_id: String,
|
||||
pub action: ApprovalAction,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ApprovalState {
|
||||
/// Channel for broadcasting approval requests to connected UI clients
|
||||
broadcast_tx: Arc<RwLock<Vec<mpsc::Sender<String>>>>,
|
||||
/// Pending approval requests awaiting user response
|
||||
pending_requests: Arc<RwLock<HashMap<String, oneshot::Sender<ApprovalAction>>>>,
|
||||
}
|
||||
|
||||
impl ApprovalState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
broadcast_tx: Arc::new(RwLock::new(Vec::new())),
|
||||
pending_requests: Arc::new(RwLock::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get or create the global approval state instance
|
||||
pub async fn global() -> Arc<Self> {
|
||||
GLOBAL_APPROVAL_STATE
|
||||
.get_or_init(|| async { Arc::new(Self::new()) })
|
||||
.await
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Submit a response to an approval request
|
||||
pub async fn submit_response(
|
||||
&self,
|
||||
request_id: String,
|
||||
action: ApprovalAction,
|
||||
) -> Result<(), String> {
|
||||
let mut pending = self.pending_requests.write().await;
|
||||
if let Some(tx) = pending.remove(&request_id) {
|
||||
tx.send(action)
|
||||
.map_err(|_| "Failed to send response".to_string())?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Request not found or already responded".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a new SSE client connection
|
||||
pub async fn add_client(&self, tx: mpsc::Sender<String>) {
|
||||
let mut senders = self.broadcast_tx.write().await;
|
||||
senders.push(tx);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ApprovalState {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement the ApprovalHandler trait from goose crate
|
||||
#[async_trait::async_trait]
|
||||
impl ApprovalHandler for ApprovalState {
|
||||
async fn request_approval(
|
||||
&self,
|
||||
session_id: String,
|
||||
approval_type: ApprovalType,
|
||||
) -> Result<ApprovalAction, String> {
|
||||
let request_id = Uuid::new_v4().to_string();
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
// store: request id -> channel so the channel can be used to continue the task when requests come back in
|
||||
let mut pending = self.pending_requests.write().await;
|
||||
pending.insert(request_id.clone(), tx);
|
||||
|
||||
let request = ApprovalRequest {
|
||||
request_id: request_id.clone(),
|
||||
session_id,
|
||||
approval_type,
|
||||
};
|
||||
|
||||
// Send the approval request to the user
|
||||
let message = serde_json::to_string(&request).map_err(|e| e.to_string())?;
|
||||
let sse_message = format!("data: {}\n\n", message);
|
||||
let senders = self.broadcast_tx.read().await;
|
||||
for sender in senders.iter() {
|
||||
let _ = sender.send(sse_message.clone()).await;
|
||||
}
|
||||
|
||||
// timeout and expire state in 5 minutes if user has not responded
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(300), rx).await {
|
||||
Ok(Ok(action)) => Ok(action),
|
||||
Ok(Err(_)) => Err("Response channel closed".to_string()),
|
||||
Err(_) => {
|
||||
// Timeout - clean up pending request
|
||||
let mut pending = self.pending_requests.write().await;
|
||||
pending.remove(&request_id);
|
||||
Err("Approval request timed out".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,6 @@ pub struct ExtensionManager {
|
||||
extensions: Mutex<HashMap<String, Extension>>,
|
||||
context: Mutex<PlatformExtensionContext>,
|
||||
provider: Arc<Mutex<Option<Arc<dyn Provider>>>>,
|
||||
approval_handler: Arc<Mutex<Option<Arc<dyn ApprovalHandler>>>>,
|
||||
}
|
||||
|
||||
/// A flattened representation of a resource used by the agent to prepare inference
|
||||
@@ -251,18 +250,9 @@ impl ExtensionManager {
|
||||
tool_route_manager: None,
|
||||
}),
|
||||
provider: Arc::new(Mutex::new(None)),
|
||||
approval_handler: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_approval_handler(&self, handler: Arc<dyn ApprovalHandler>) {
|
||||
*self.approval_handler.lock().await = Some(handler);
|
||||
}
|
||||
|
||||
pub async fn get_approval_handler(&self) -> Option<Arc<dyn ApprovalHandler>> {
|
||||
self.approval_handler.lock().await.clone()
|
||||
}
|
||||
|
||||
pub async fn set_context(&self, context: PlatformExtensionContext) {
|
||||
*self.context.lock().await = context;
|
||||
}
|
||||
@@ -348,9 +338,9 @@ impl ExtensionManager {
|
||||
let mut sampling_handler =
|
||||
ExtensionSamplingHandler::new(self.provider.clone(), sanitized_name.clone());
|
||||
|
||||
if let Some(approval_handler) = self.get_approval_handler().await {
|
||||
sampling_handler = sampling_handler.with_approval_handler(approval_handler);
|
||||
}
|
||||
// Use the global approval state
|
||||
let approval_handler = super::approval::ApprovalState::global().await;
|
||||
sampling_handler = sampling_handler.with_approval_handler(approval_handler);
|
||||
|
||||
let sampling_handler = Box::new(sampling_handler);
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ impl From<ToolResult<Vec<Content>>> for ToolCallResult {
|
||||
}
|
||||
|
||||
use super::agent::{tool_stream, ToolStream};
|
||||
use crate::agents::{Agent, SessionConfig};
|
||||
use crate::agents::{Agent, ApprovalHandler, SessionConfig};
|
||||
use crate::conversation::message::{Message, ToolRequest};
|
||||
use crate::tool_inspection::get_security_finding_id_from_results;
|
||||
|
||||
@@ -77,28 +77,24 @@ impl Agent {
|
||||
);
|
||||
yield confirmation;
|
||||
|
||||
// Use the unified approval handler system
|
||||
let approval_handler = self.extension_manager.get_approval_handler().await;
|
||||
let approval_action = if let Some(handler) = approval_handler {
|
||||
let session_id = session.as_ref()
|
||||
.map(|s| s.id.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
match handler.request_approval(
|
||||
session_id,
|
||||
crate::agents::approval::ApprovalType::ToolCall {
|
||||
tool_name: tool_call.name.to_string(),
|
||||
prompt: security_message,
|
||||
principal_type: "Tool".to_string(),
|
||||
}
|
||||
).await {
|
||||
Ok(action) => Some(action),
|
||||
Err(e) => {
|
||||
tracing::error!("Approval handler error: {}", e);
|
||||
None
|
||||
}
|
||||
let approval_handler = crate::agents::approval::ApprovalState::global().await;
|
||||
let session_id = session.as_ref()
|
||||
.map(|s| s.id.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
let approval_action = match approval_handler.as_ref().request_approval(
|
||||
session_id,
|
||||
crate::agents::approval::ApprovalType::ToolCall {
|
||||
tool_name: tool_call.name.to_string(),
|
||||
prompt: security_message,
|
||||
principal_type: "Tool".to_string(),
|
||||
}
|
||||
).await {
|
||||
Ok(action) => Some(action),
|
||||
Err(e) => {
|
||||
tracing::error!("Approval handler error: {}", e);
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(action) = approval_action {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::agents::approval::ApprovalHandler;
|
||||
use crate::agents::extension::PlatformExtensionContext;
|
||||
use crate::agents::Agent;
|
||||
use crate::config::paths::Paths;
|
||||
@@ -21,7 +20,6 @@ pub struct AgentManager {
|
||||
sessions: Arc<RwLock<LruCache<String, Arc<Agent>>>>,
|
||||
scheduler: Arc<dyn SchedulerTrait>,
|
||||
default_provider: Arc<RwLock<Option<Arc<dyn crate::providers::base::Provider>>>>,
|
||||
approval_handler: Arc<RwLock<Option<Arc<dyn ApprovalHandler>>>>,
|
||||
}
|
||||
|
||||
impl AgentManager {
|
||||
@@ -49,7 +47,6 @@ impl AgentManager {
|
||||
sessions: Arc::new(RwLock::new(LruCache::new(capacity))),
|
||||
scheduler,
|
||||
default_provider: Arc::new(RwLock::new(None)),
|
||||
approval_handler: Arc::new(RwLock::new(None)),
|
||||
};
|
||||
|
||||
let _ = manager.configure_default_provider().await;
|
||||
@@ -76,11 +73,6 @@ impl AgentManager {
|
||||
*self.default_provider.write().await = Some(provider);
|
||||
}
|
||||
|
||||
pub async fn set_approval_handler(&self, handler: Arc<dyn ApprovalHandler>) {
|
||||
debug!("Setting approval handler on AgentManager");
|
||||
*self.approval_handler.write().await = Some(handler);
|
||||
}
|
||||
|
||||
pub async fn configure_default_provider(&self) -> Result<()> {
|
||||
let provider_name = std::env::var("GOOSE_DEFAULT_PROVIDER")
|
||||
.or_else(|_| std::env::var("GOOSE_PROVIDER__TYPE"))
|
||||
@@ -135,12 +127,6 @@ impl AgentManager {
|
||||
if let Some(provider) = &*self.default_provider.read().await {
|
||||
agent.update_provider(Arc::clone(provider)).await?;
|
||||
}
|
||||
if let Some(handler) = &*self.approval_handler.read().await {
|
||||
agent
|
||||
.extension_manager
|
||||
.set_approval_handler(Arc::clone(handler))
|
||||
.await;
|
||||
}
|
||||
|
||||
let mut sessions = self.sessions.write().await;
|
||||
if let Some(existing) = sessions.get(&session_id) {
|
||||
|
||||
@@ -2081,12 +2081,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"description": "A request for user approval"
|
||||
]
|
||||
},
|
||||
"ApprovalResponse": {
|
||||
"type": "object",
|
||||
"description": "A response to an approval request",
|
||||
"required": [
|
||||
"requestId",
|
||||
"action"
|
||||
|
||||
@@ -12,17 +12,11 @@ export type Annotations = {
|
||||
|
||||
export type ApprovalAction = 'allow_once' | 'always_allow' | 'deny';
|
||||
|
||||
/**
|
||||
* A request for user approval
|
||||
*/
|
||||
export type ApprovalRequest = ApprovalType & {
|
||||
requestId: string;
|
||||
sessionId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A response to an approval request
|
||||
*/
|
||||
export type ApprovalResponse = {
|
||||
action: ApprovalAction;
|
||||
requestId: string;
|
||||
|
||||
Reference in New Issue
Block a user