fix: require auth when running goose on non loopback address (#6478)
This commit is contained in:
@@ -832,6 +832,13 @@ enum Command {
|
|||||||
/// Authentication token for both Basic Auth (password) and Bearer token
|
/// Authentication token for both Basic Auth (password) and Bearer token
|
||||||
#[arg(long, help = "Authentication token to secure the web interface")]
|
#[arg(long, help = "Authentication token to secure the web interface")]
|
||||||
auth_token: Option<String>,
|
auth_token: Option<String>,
|
||||||
|
|
||||||
|
/// Allow running without authentication when exposed on the network (unsafe)
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "Skip auth requirement when exposed on the network (unsafe)"
|
||||||
|
)]
|
||||||
|
no_auth: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Terminal-integrated session (one session per terminal)
|
/// Terminal-integrated session (one session per terminal)
|
||||||
@@ -1520,7 +1527,8 @@ pub async fn cli() -> anyhow::Result<()> {
|
|||||||
host,
|
host,
|
||||||
open,
|
open,
|
||||||
auth_token,
|
auth_token,
|
||||||
}) => crate::commands::web::handle_web(port, host, open, auth_token).await,
|
no_auth,
|
||||||
|
}) => crate::commands::web::handle_web(port, host, open, auth_token, no_auth).await,
|
||||||
Some(Command::Term { command }) => handle_term_subcommand(command).await,
|
Some(Command::Term { command }) => handle_term_subcommand(command).await,
|
||||||
None => handle_default_session().await,
|
None => handle_default_session().await,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ use goose::session::session_manager::SessionType;
|
|||||||
use goose::session::SessionManager;
|
use goose::session::SessionManager;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::{net::SocketAddr, sync::Arc};
|
use std::{net::ToSocketAddrs, sync::Arc};
|
||||||
use tokio::sync::{Mutex, RwLock};
|
use tokio::sync::{Mutex, RwLock};
|
||||||
use tower_http::cors::{AllowOrigin, Any, CorsLayer};
|
use tower_http::cors::{AllowOrigin, Any, CorsLayer};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
@@ -125,14 +125,28 @@ async fn auth_middleware(
|
|||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_web(
|
fn is_loopback_address(host: &str) -> bool {
|
||||||
port: u16,
|
(host, 0)
|
||||||
host: String,
|
.to_socket_addrs()
|
||||||
open: bool,
|
.map(|mut addrs| addrs.any(|addr| addr.ip().is_loopback()))
|
||||||
auth_token: Option<String>,
|
.unwrap_or(false)
|
||||||
) -> Result<()> {
|
}
|
||||||
crate::logging::setup_logging(Some("goose-web"), None)?;
|
|
||||||
|
|
||||||
|
fn validate_network_auth(host: &str, auth_token: &Option<String>, no_auth: bool) {
|
||||||
|
if !is_loopback_address(host) && auth_token.is_none() && !no_auth {
|
||||||
|
eprintln!(
|
||||||
|
"Error: --auth-token is required when the server is exposed on the network ({}).",
|
||||||
|
host
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
"For security, use --auth-token <TOKEN> or bind to a local address (e.g., localhost)."
|
||||||
|
);
|
||||||
|
eprintln!("To skip this check, use --no-auth (unsafe).");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_provider_and_model() -> (String, String) {
|
||||||
let config = goose::config::Config::global();
|
let config = goose::config::Config::global();
|
||||||
|
|
||||||
let provider_name: String = match config.get_goose_provider() {
|
let provider_name: String = match config.get_goose_provider() {
|
||||||
@@ -151,7 +165,11 @@ pub async fn handle_web(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let model_config = goose::model::ModelConfig::new(&model)?;
|
(provider_name, model)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_agent(provider_name: &str, model: &str) -> Result<Agent> {
|
||||||
|
let model_config = goose::model::ModelConfig::new(model)?;
|
||||||
|
|
||||||
let init_session = SessionManager::create_session(
|
let init_session = SessionManager::create_session(
|
||||||
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
|
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
|
||||||
@@ -161,7 +179,7 @@ pub async fn handle_web(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let agent = Agent::new();
|
let agent = Agent::new();
|
||||||
let provider = goose::providers::create(&provider_name, model_config).await?;
|
let provider = goose::providers::create(provider_name, model_config).await?;
|
||||||
agent.update_provider(provider, &init_session.id).await?;
|
agent.update_provider(provider, &init_session.id).await?;
|
||||||
|
|
||||||
let enabled_configs = goose::config::get_enabled_extensions();
|
let enabled_configs = goose::config::get_enabled_extensions();
|
||||||
@@ -171,20 +189,11 @@ pub async fn handle_web(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let ws_token = if auth_token.is_none() {
|
Ok(agent)
|
||||||
uuid::Uuid::new_v4().to_string()
|
}
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
let state = AppState {
|
fn build_cors_layer(auth_token: &Option<String>, host: &str, port: u16) -> CorsLayer {
|
||||||
agent: Arc::new(agent),
|
if auth_token.is_none() {
|
||||||
cancellations: Arc::new(RwLock::new(std::collections::HashMap::new())),
|
|
||||||
auth_token: auth_token.clone(),
|
|
||||||
ws_token,
|
|
||||||
};
|
|
||||||
|
|
||||||
let cors_layer = if auth_token.is_none() {
|
|
||||||
let allowed_origins = [
|
let allowed_origins = [
|
||||||
"http://localhost:3000".parse().unwrap(),
|
"http://localhost:3000".parse().unwrap(),
|
||||||
"http://127.0.0.1:3000".parse().unwrap(),
|
"http://127.0.0.1:3000".parse().unwrap(),
|
||||||
@@ -199,9 +208,11 @@ pub async fn handle_web(
|
|||||||
.allow_origin(Any)
|
.allow_origin(Any)
|
||||||
.allow_methods(Any)
|
.allow_methods(Any)
|
||||||
.allow_headers(Any)
|
.allow_headers(Any)
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let app = Router::new()
|
fn build_router(state: AppState, cors_layer: CorsLayer) -> Router {
|
||||||
|
Router::new()
|
||||||
.route("/", get(serve_index))
|
.route("/", get(serve_index))
|
||||||
.route("/session/{session_name}", get(serve_session))
|
.route("/session/{session_name}", get(serve_session))
|
||||||
.route("/ws", get(websocket_handler))
|
.route("/ws", get(websocket_handler))
|
||||||
@@ -214,9 +225,42 @@ pub async fn handle_web(
|
|||||||
auth_middleware,
|
auth_middleware,
|
||||||
))
|
))
|
||||||
.layer(cors_layer)
|
.layer(cors_layer)
|
||||||
.with_state(state);
|
.with_state(state)
|
||||||
|
}
|
||||||
|
|
||||||
let addr: SocketAddr = format!("{}:{}", host, port).parse()?;
|
pub async fn handle_web(
|
||||||
|
port: u16,
|
||||||
|
host: String,
|
||||||
|
open: bool,
|
||||||
|
auth_token: Option<String>,
|
||||||
|
no_auth: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
validate_network_auth(&host, &auth_token, no_auth);
|
||||||
|
crate::logging::setup_logging(Some("goose-web"), None)?;
|
||||||
|
|
||||||
|
let (provider_name, model) = get_provider_and_model();
|
||||||
|
let agent = create_agent(&provider_name, &model).await?;
|
||||||
|
|
||||||
|
let ws_token = if auth_token.is_none() {
|
||||||
|
uuid::Uuid::new_v4().to_string()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
let state = AppState {
|
||||||
|
agent: Arc::new(agent),
|
||||||
|
cancellations: Arc::new(RwLock::new(std::collections::HashMap::new())),
|
||||||
|
auth_token: auth_token.clone(),
|
||||||
|
ws_token,
|
||||||
|
};
|
||||||
|
|
||||||
|
let cors_layer = build_cors_layer(&auth_token, &host, port);
|
||||||
|
let app = build_router(state, cors_layer);
|
||||||
|
|
||||||
|
let addr = (host.as_str(), port)
|
||||||
|
.to_socket_addrs()?
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Could not resolve address: {}", host))?;
|
||||||
|
|
||||||
println!("\n🪿 Starting goose web server");
|
println!("\n🪿 Starting goose web server");
|
||||||
println!(" Provider: {} | Model: {}", provider_name, model);
|
println!(" Provider: {} | Model: {}", provider_name, model);
|
||||||
@@ -373,98 +417,7 @@ async fn handle_socket(socket: WebSocket, state: AppState) {
|
|||||||
if let Ok(msg) = msg {
|
if let Ok(msg) = msg {
|
||||||
match msg {
|
match msg {
|
||||||
Message::Text(text) => {
|
Message::Text(text) => {
|
||||||
match serde_json::from_str::<WebSocketMessage>(&text.to_string()) {
|
handle_text_message(&text.to_string(), &sender, &state).await;
|
||||||
Ok(WebSocketMessage::Message {
|
|
||||||
content,
|
|
||||||
session_id,
|
|
||||||
..
|
|
||||||
}) => {
|
|
||||||
let sender_clone = sender.clone();
|
|
||||||
let agent = state.agent.clone();
|
|
||||||
let session_id_clone = session_id.clone();
|
|
||||||
|
|
||||||
let task_handle = tokio::spawn(async move {
|
|
||||||
let result = process_message_streaming(
|
|
||||||
&agent,
|
|
||||||
session_id_clone,
|
|
||||||
content,
|
|
||||||
sender_clone,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Err(e) = result {
|
|
||||||
error!("Error processing message: {}", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut cancellations = state.cancellations.write().await;
|
|
||||||
cancellations
|
|
||||||
.insert(session_id.clone(), task_handle.abort_handle());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle task completion and cleanup
|
|
||||||
let sender_for_abort = sender.clone();
|
|
||||||
let session_id_for_cleanup = session_id.clone();
|
|
||||||
let cancellations_for_cleanup = state.cancellations.clone();
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
match task_handle.await {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(e) if e.is_cancelled() => {
|
|
||||||
let mut sender = sender_for_abort.lock().await;
|
|
||||||
let _ = sender
|
|
||||||
.send(Message::Text(
|
|
||||||
serde_json::to_string(
|
|
||||||
&WebSocketMessage::Cancelled {
|
|
||||||
message: "Operation cancelled by user"
|
|
||||||
.to_string(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.into(),
|
|
||||||
))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Task error: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut cancellations = cancellations_for_cleanup.write().await;
|
|
||||||
cancellations.remove(&session_id_for_cleanup);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(WebSocketMessage::Cancel { session_id }) => {
|
|
||||||
// Cancel the active operation for this session
|
|
||||||
let abort_handle = {
|
|
||||||
let mut cancellations = state.cancellations.write().await;
|
|
||||||
cancellations.remove(&session_id)
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(handle) = abort_handle {
|
|
||||||
handle.abort();
|
|
||||||
|
|
||||||
// Send cancellation confirmation
|
|
||||||
let mut sender = sender.lock().await;
|
|
||||||
let _ = sender
|
|
||||||
.send(Message::Text(
|
|
||||||
serde_json::to_string(&WebSocketMessage::Cancelled {
|
|
||||||
message: "Operation cancelled".to_string(),
|
|
||||||
})
|
|
||||||
.unwrap()
|
|
||||||
.into(),
|
|
||||||
))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(_) => {
|
|
||||||
// Ignore other message types
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to parse WebSocket message: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Message::Close(_) => break,
|
Message::Close(_) => break,
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -475,15 +428,101 @@ async fn handle_socket(socket: WebSocket, state: AppState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_text_message(
|
||||||
|
text: &str,
|
||||||
|
sender: &Arc<Mutex<futures::stream::SplitSink<WebSocket, Message>>>,
|
||||||
|
state: &AppState,
|
||||||
|
) {
|
||||||
|
match serde_json::from_str::<WebSocketMessage>(text) {
|
||||||
|
Ok(WebSocketMessage::Message {
|
||||||
|
content,
|
||||||
|
session_id,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
handle_user_message(content, session_id, sender.clone(), state).await;
|
||||||
|
}
|
||||||
|
Ok(WebSocketMessage::Cancel { session_id }) => {
|
||||||
|
handle_cancel_message(session_id, sender, state).await;
|
||||||
|
}
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to parse WebSocket message: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_user_message(
|
||||||
|
content: String,
|
||||||
|
session_id: String,
|
||||||
|
sender: Arc<Mutex<futures::stream::SplitSink<WebSocket, Message>>>,
|
||||||
|
state: &AppState,
|
||||||
|
) {
|
||||||
|
let agent = state.agent.clone();
|
||||||
|
let session_id_clone = session_id.clone();
|
||||||
|
|
||||||
|
let task_handle = tokio::spawn(async move {
|
||||||
|
let result = process_message_streaming(&agent, session_id_clone, content, sender).await;
|
||||||
|
|
||||||
|
if let Err(e) = result {
|
||||||
|
error!("Error processing message: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut cancellations = state.cancellations.write().await;
|
||||||
|
cancellations.insert(session_id.clone(), task_handle.abort_handle());
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancellations_for_cleanup = state.cancellations.clone();
|
||||||
|
let session_id_for_cleanup = session_id;
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) = task_handle.await {
|
||||||
|
if e.is_cancelled() {
|
||||||
|
tracing::debug!("Task was cancelled");
|
||||||
|
} else {
|
||||||
|
error!("Task error: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cancellations = cancellations_for_cleanup.write().await;
|
||||||
|
cancellations.remove(&session_id_for_cleanup);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_cancel_message(
|
||||||
|
session_id: String,
|
||||||
|
sender: &Arc<Mutex<futures::stream::SplitSink<WebSocket, Message>>>,
|
||||||
|
state: &AppState,
|
||||||
|
) {
|
||||||
|
let abort_handle = {
|
||||||
|
let mut cancellations = state.cancellations.write().await;
|
||||||
|
cancellations.remove(&session_id)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(handle) = abort_handle {
|
||||||
|
handle.abort();
|
||||||
|
|
||||||
|
let mut sender = sender.lock().await;
|
||||||
|
let _ = sender
|
||||||
|
.send(Message::Text(
|
||||||
|
serde_json::to_string(&WebSocketMessage::Cancelled {
|
||||||
|
message: "Operation cancelled".to_string(),
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.into(),
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn process_message_streaming(
|
async fn process_message_streaming(
|
||||||
agent: &Agent,
|
agent: &Agent,
|
||||||
session_id: String,
|
session_id: String,
|
||||||
content: String,
|
content: String,
|
||||||
sender: Arc<Mutex<futures::stream::SplitSink<WebSocket, Message>>>,
|
sender: Arc<Mutex<futures::stream::SplitSink<WebSocket, Message>>>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
use futures::StreamExt;
|
|
||||||
use goose::agents::SessionConfig;
|
use goose::agents::SessionConfig;
|
||||||
use goose::conversation::message::MessageContent;
|
|
||||||
|
|
||||||
let user_message = GooseMessage::user().with_text(content.clone());
|
let user_message = GooseMessage::user().with_text(content.clone());
|
||||||
|
|
||||||
@@ -521,93 +560,12 @@ async fn process_message_streaming(
|
|||||||
while let Some(result) = stream.next().await {
|
while let Some(result) = stream.next().await {
|
||||||
match result {
|
match result {
|
||||||
Ok(AgentEvent::Message(message)) => {
|
Ok(AgentEvent::Message(message)) => {
|
||||||
for content in &message.content {
|
process_agent_message(&message, &sender, agent).await;
|
||||||
match content {
|
|
||||||
MessageContent::Text(text) => {
|
|
||||||
let mut sender = sender.lock().await;
|
|
||||||
let _ = sender
|
|
||||||
.send(Message::Text(
|
|
||||||
serde_json::to_string(&WebSocketMessage::Response {
|
|
||||||
content: text.text.clone(),
|
|
||||||
role: "assistant".to_string(),
|
|
||||||
timestamp: chrono::Utc::now().timestamp_millis(),
|
|
||||||
})
|
|
||||||
.unwrap()
|
|
||||||
.into(),
|
|
||||||
))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
MessageContent::ToolRequest(req) => {
|
|
||||||
let mut sender = sender.lock().await;
|
|
||||||
if let Ok(tool_call) = &req.tool_call {
|
|
||||||
let _ = sender
|
|
||||||
.send(Message::Text(
|
|
||||||
serde_json::to_string(
|
|
||||||
&WebSocketMessage::ToolRequest {
|
|
||||||
id: req.id.clone(),
|
|
||||||
tool_name: tool_call.name.to_string(),
|
|
||||||
arguments: Value::from(
|
|
||||||
tool_call.arguments.clone(),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.into(),
|
|
||||||
))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MessageContent::ToolResponse(_resp) => {}
|
|
||||||
MessageContent::ToolConfirmationRequest(confirmation) => {
|
|
||||||
let mut sender = sender.lock().await;
|
|
||||||
let _ = sender
|
|
||||||
.send(Message::Text(
|
|
||||||
serde_json::to_string(
|
|
||||||
&WebSocketMessage::ToolConfirmation {
|
|
||||||
id: confirmation.id.clone(),
|
|
||||||
tool_name: confirmation
|
|
||||||
.tool_name
|
|
||||||
.to_string()
|
|
||||||
.clone(),
|
|
||||||
arguments: Value::from(
|
|
||||||
confirmation.arguments.clone(),
|
|
||||||
),
|
|
||||||
needs_confirmation: true,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.into(),
|
|
||||||
))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
agent.handle_confirmation(
|
|
||||||
confirmation.id.clone(),
|
|
||||||
goose::permission::PermissionConfirmation {
|
|
||||||
principal_type: goose::permission::permission_confirmation::PrincipalType::Tool,
|
|
||||||
permission: goose::permission::Permission::AllowOnce,
|
|
||||||
}
|
|
||||||
).await;
|
|
||||||
}
|
|
||||||
MessageContent::Thinking(thinking) => {
|
|
||||||
let mut sender = sender.lock().await;
|
|
||||||
let _ = sender
|
|
||||||
.send(Message::Text(
|
|
||||||
serde_json::to_string(&WebSocketMessage::Thinking {
|
|
||||||
message: thinking.thinking.clone(),
|
|
||||||
})
|
|
||||||
.unwrap()
|
|
||||||
.into(),
|
|
||||||
))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(AgentEvent::HistoryReplaced(_new_messages)) => {
|
Ok(AgentEvent::HistoryReplaced(_)) => {
|
||||||
tracing::info!("History replaced, compacting happened in reply");
|
tracing::info!("History replaced, compacting happened in reply");
|
||||||
}
|
}
|
||||||
Ok(AgentEvent::McpNotification(_notification)) => {
|
Ok(AgentEvent::McpNotification(_)) => {
|
||||||
tracing::info!("Received MCP notification in web interface");
|
tracing::info!("Received MCP notification in web interface");
|
||||||
}
|
}
|
||||||
Ok(AgentEvent::ModelChange { model, mode }) => {
|
Ok(AgentEvent::ModelChange { model, mode }) => {
|
||||||
@@ -615,16 +573,7 @@ async fn process_message_streaming(
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error in message stream: {}", e);
|
error!("Error in message stream: {}", e);
|
||||||
let mut sender = sender.lock().await;
|
send_error(&sender, &format!("Error: {}", e)).await;
|
||||||
let _ = sender
|
|
||||||
.send(Message::Text(
|
|
||||||
serde_json::to_string(&WebSocketMessage::Error {
|
|
||||||
message: format!("Error: {}", e),
|
|
||||||
})
|
|
||||||
.unwrap()
|
|
||||||
.into(),
|
|
||||||
))
|
|
||||||
.await;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -632,16 +581,7 @@ async fn process_message_streaming(
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error calling agent: {}", e);
|
error!("Error calling agent: {}", e);
|
||||||
let mut sender = sender.lock().await;
|
send_error(&sender, &format!("Error: {}", e)).await;
|
||||||
let _ = sender
|
|
||||||
.send(Message::Text(
|
|
||||||
serde_json::to_string(&WebSocketMessage::Error {
|
|
||||||
message: format!("Error: {}", e),
|
|
||||||
})
|
|
||||||
.unwrap()
|
|
||||||
.into(),
|
|
||||||
))
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -658,3 +598,104 @@ async fn process_message_streaming(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn process_agent_message(
|
||||||
|
message: &GooseMessage,
|
||||||
|
sender: &Arc<Mutex<futures::stream::SplitSink<WebSocket, Message>>>,
|
||||||
|
agent: &Agent,
|
||||||
|
) {
|
||||||
|
use goose::conversation::message::MessageContent;
|
||||||
|
|
||||||
|
for content in &message.content {
|
||||||
|
match content {
|
||||||
|
MessageContent::Text(text) => {
|
||||||
|
let mut sender = sender.lock().await;
|
||||||
|
let _ = sender
|
||||||
|
.send(Message::Text(
|
||||||
|
serde_json::to_string(&WebSocketMessage::Response {
|
||||||
|
content: text.text.clone(),
|
||||||
|
role: "assistant".to_string(),
|
||||||
|
timestamp: chrono::Utc::now().timestamp_millis(),
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.into(),
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
MessageContent::ToolRequest(req) => {
|
||||||
|
let mut sender = sender.lock().await;
|
||||||
|
if let Ok(tool_call) = &req.tool_call {
|
||||||
|
let _ = sender
|
||||||
|
.send(Message::Text(
|
||||||
|
serde_json::to_string(&WebSocketMessage::ToolRequest {
|
||||||
|
id: req.id.clone(),
|
||||||
|
tool_name: tool_call.name.to_string(),
|
||||||
|
arguments: Value::from(tool_call.arguments.clone()),
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.into(),
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MessageContent::ToolResponse(_) => {}
|
||||||
|
MessageContent::ToolConfirmationRequest(confirmation) => {
|
||||||
|
{
|
||||||
|
let mut sender = sender.lock().await;
|
||||||
|
let _ = sender
|
||||||
|
.send(Message::Text(
|
||||||
|
serde_json::to_string(&WebSocketMessage::ToolConfirmation {
|
||||||
|
id: confirmation.id.clone(),
|
||||||
|
tool_name: confirmation.tool_name.to_string(),
|
||||||
|
arguments: Value::from(confirmation.arguments.clone()),
|
||||||
|
needs_confirmation: true,
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.into(),
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
agent
|
||||||
|
.handle_confirmation(
|
||||||
|
confirmation.id.clone(),
|
||||||
|
goose::permission::PermissionConfirmation {
|
||||||
|
principal_type:
|
||||||
|
goose::permission::permission_confirmation::PrincipalType::Tool,
|
||||||
|
permission: goose::permission::Permission::AllowOnce,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
MessageContent::Thinking(thinking) => {
|
||||||
|
let mut sender = sender.lock().await;
|
||||||
|
let _ = sender
|
||||||
|
.send(Message::Text(
|
||||||
|
serde_json::to_string(&WebSocketMessage::Thinking {
|
||||||
|
message: thinking.thinking.clone(),
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.into(),
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_error(
|
||||||
|
sender: &Arc<Mutex<futures::stream::SplitSink<WebSocket, Message>>>,
|
||||||
|
message: &str,
|
||||||
|
) {
|
||||||
|
let mut sender = sender.lock().await;
|
||||||
|
let _ = sender
|
||||||
|
.send(Message::Text(
|
||||||
|
serde_json::to_string(&WebSocketMessage::Error {
|
||||||
|
message: message.to_string(),
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.into(),
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user