This commit is contained in:
Alex Hancock
2025-10-22 10:32:28 -04:00
parent a8559f2241
commit 981f149ce6
12 changed files with 152 additions and 180 deletions
Generated
+1
View File
@@ -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,
+5 -111
View File
@@ -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",
+5 -2
View File
@@ -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))
}
-9
View File
@@ -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,
}))
}
+1
View File
@@ -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"] }
+117
View File
@@ -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())
}
}
}
}
+3 -13
View File
@@ -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);
+18 -22
View File
@@ -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 {
-14
View File
@@ -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) {
+1 -3
View File
@@ -2081,12 +2081,10 @@
}
}
}
],
"description": "A request for user approval"
]
},
"ApprovalResponse": {
"type": "object",
"description": "A response to an approval request",
"required": [
"requestId",
"action"
-6
View File
@@ -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;