From bf7921bf299cfdea45a0e5ce482bcb7c5966608e Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Mon, 25 Aug 2025 23:33:26 -0400 Subject: [PATCH 1/8] Use middleware to check secret key # Conflicts: # crates/goose-server/src/commands/agent.rs # crates/goose-server/src/routes/agent.rs # crates/goose-server/src/routes/audio.rs # crates/goose-server/src/routes/config_management.rs # crates/goose-server/src/routes/context.rs # crates/goose-server/src/routes/extension.rs # crates/goose-server/src/routes/reply.rs # crates/goose-server/src/routes/session.rs # crates/goose-server/src/state.rs # crates/goose-server/tests/pricing_api_test.rs --- crates/goose-server/src/auth.rs | 22 +++ crates/goose-server/src/commands/agent.rs | 8 +- crates/goose-server/src/lib.rs | 1 + crates/goose-server/src/routes/agent.rs | 36 +---- crates/goose-server/src/routes/audio.rs | 35 +---- .../src/routes/config_management.rs | 125 +++--------------- crates/goose-server/src/routes/context.rs | 11 +- crates/goose-server/src/routes/extension.rs | 9 +- crates/goose-server/src/routes/mod.rs | 1 - crates/goose-server/src/routes/reply.rs | 14 +- crates/goose-server/src/routes/schedule.rs | 23 +--- crates/goose-server/src/routes/session.rs | 27 +--- crates/goose-server/src/routes/utils.rs | 16 --- crates/goose-server/src/state.rs | 4 +- crates/goose-server/tests/pricing_api_test.rs | 2 +- ui/desktop/src/goosed.ts | 29 ++-- 16 files changed, 81 insertions(+), 282 deletions(-) create mode 100644 crates/goose-server/src/auth.rs diff --git a/crates/goose-server/src/auth.rs b/crates/goose-server/src/auth.rs new file mode 100644 index 000000000000..3c6dd6602e46 --- /dev/null +++ b/crates/goose-server/src/auth.rs @@ -0,0 +1,22 @@ +use axum::{ + extract::{Request, State}, + http::StatusCode, + middleware::Next, + response::Response, +}; + +pub async fn layer_fn( + State(state): State, + request: Request, + next: Next, +) -> Result { + let secret_key = request + .headers() + .get("X-Secret-Key") + .and_then(|value| value.to_str().ok()); + + match secret_key { + Some(key) if key == state => Ok(next.run(request).await), + _ => Err(StatusCode::UNAUTHORIZED), + } +} diff --git a/crates/goose-server/src/commands/agent.rs b/crates/goose-server/src/commands/agent.rs index b9ab64f41c35..d300cef5a4ee 100644 --- a/crates/goose-server/src/commands/agent.rs +++ b/crates/goose-server/src/commands/agent.rs @@ -3,10 +3,12 @@ use std::sync::Arc; use crate::configuration; use crate::state; use anyhow::Result; +use axum::middleware; use etcetera::{choose_app_strategy, AppStrategy}; use goose::agents::Agent; use goose::config::APP_STRATEGY; use goose::scheduler_factory::SchedulerFactory; +use goose_server::auth::layer_fn; use tower_http::cors::{Any, CorsLayer}; use tracing::info; @@ -33,7 +35,7 @@ pub async fn run() -> Result<()> { let new_agent = Agent::new(); let agent_ref = Arc::new(new_agent); - let app_state = state::AppState::new(agent_ref.clone(), secret_key.clone()); + let app_state = state::AppState::new(agent_ref.clone()); let schedule_file_path = choose_app_strategy(APP_STRATEGY.clone())? .data_dir() @@ -50,7 +52,9 @@ pub async fn run() -> Result<()> { .allow_methods(Any) .allow_headers(Any); - let app = crate::routes::configure(app_state).layer(cors); + let app = crate::routes::configure(app_state) + .layer(middleware::from_fn_with_state(secret_key.clone(), layer_fn)) + .layer(cors); let listener = tokio::net::TcpListener::bind(settings.socket_addr()).await?; info!("listening on {}", listener.local_addr()?); diff --git a/crates/goose-server/src/lib.rs b/crates/goose-server/src/lib.rs index 36c83824c45b..b945da8ce595 100644 --- a/crates/goose-server/src/lib.rs +++ b/crates/goose-server/src/lib.rs @@ -1,3 +1,4 @@ +pub mod auth; pub mod openapi; pub mod routes; pub mod state; diff --git a/crates/goose-server/src/routes/agent.rs b/crates/goose-server/src/routes/agent.rs index 2a5911826227..2a0ffbdf32d3 100644 --- a/crates/goose-server/src/routes/agent.rs +++ b/crates/goose-server/src/routes/agent.rs @@ -1,9 +1,8 @@ -use super::utils::verify_secret_key; use crate::state::AppState; use axum::response::IntoResponse; use axum::{ extract::{Query, State}, - http::{HeaderMap, StatusCode}, + http::StatusCode, routing::{get, post}, Json, Router, }; @@ -115,11 +114,8 @@ pub struct ErrorResponse { )] async fn start_agent( State(state): State>, - headers: HeaderMap, Json(payload): Json, ) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - state.reset().await; let session_id = session::generate_session_id(); @@ -168,12 +164,8 @@ async fn start_agent( ) )] async fn resume_agent( - State(state): State>, - headers: HeaderMap, Json(payload): Json, ) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - let session_path = match session::get_path(session::Identifier::Name(payload.session_id.clone())) { Ok(path) => path, @@ -209,11 +201,8 @@ async fn resume_agent( )] async fn add_sub_recipes( State(state): State>, - headers: HeaderMap, Json(payload): Json, ) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - let agent = state.get_agent().await; agent.add_sub_recipes(payload.sub_recipes.clone()).await; Ok(Json(AddSubRecipesResponse { success: true })) @@ -231,11 +220,8 @@ async fn add_sub_recipes( )] async fn extend_prompt( State(state): State>, - headers: HeaderMap, Json(payload): Json, ) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - let agent = state.get_agent().await; agent.extend_system_prompt(payload.extension.clone()).await; Ok(Json(ExtendPromptResponse { success: true })) @@ -257,11 +243,8 @@ async fn extend_prompt( )] async fn get_tools( State(state): State>, - headers: HeaderMap, Query(query): Query, ) -> Result>, StatusCode> { - verify_secret_key(&headers, &state)?; - let config = Config::global(); let goose_mode = config.get_param("GOOSE_MODE").unwrap_or("auto".to_string()); let agent = state.get_agent().await; @@ -314,11 +297,8 @@ async fn get_tools( )] async fn update_agent_provider( State(state): State>, - headers: HeaderMap, Json(payload): Json, ) -> Result { - verify_secret_key(&headers, &state).map_err(|e| (e, String::new()))?; - let agent = state.get_agent().await; let config = Config::global(); let model = match payload @@ -364,15 +344,8 @@ async fn update_agent_provider( )] async fn update_router_tool_selector( State(state): State>, - headers: HeaderMap, Json(_payload): Json, ) -> Result, Json> { - verify_secret_key(&headers, &state).map_err(|_| { - Json(ErrorResponse { - error: "Unauthorized - Invalid or missing API key".to_string(), - }) - })?; - let agent = state.get_agent().await; agent .update_router_tool_selector(None, Some(true)) @@ -402,15 +375,8 @@ async fn update_router_tool_selector( )] async fn update_session_config( State(state): State>, - headers: HeaderMap, Json(payload): Json, ) -> Result, Json> { - verify_secret_key(&headers, &state).map_err(|_| { - Json(ErrorResponse { - error: "Unauthorized - Invalid or missing API key".to_string(), - }) - })?; - let agent = state.get_agent().await; if let Some(response) = payload.response { agent.add_final_output_tool(response).await; diff --git a/crates/goose-server/src/routes/audio.rs b/crates/goose-server/src/routes/audio.rs index 3ab4d8b83dd7..fefddc54e446 100644 --- a/crates/goose-server/src/routes/audio.rs +++ b/crates/goose-server/src/routes/audio.rs @@ -2,11 +2,9 @@ /// /// This module provides endpoints for audio transcription using OpenAI's Whisper API. /// The OpenAI API key must be configured in the backend for this to work. -use super::utils::verify_secret_key; use crate::state::AppState; use axum::{ - extract::State, - http::{HeaderMap, StatusCode}, + http::StatusCode, routing::{get, post}, Json, Router, }; @@ -237,12 +235,8 @@ async fn transcribe_handler( /// Uses ElevenLabs' speech-to-text endpoint for transcription. /// Requires an ElevenLabs API key with speech-to-text access. async fn transcribe_elevenlabs_handler( - State(state): State>, - headers: HeaderMap, Json(request): Json, ) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - let (audio_bytes, file_extension) = validate_audio_input(&request.audio, &request.mime_type)?; // Get the ElevenLabs API key from config (after input validation) @@ -369,12 +363,7 @@ async fn transcribe_elevenlabs_handler( /// Check if dictation providers are configured /// /// Returns configuration status for dictation providers -async fn check_dictation_config( - State(state): State>, - headers: HeaderMap, -) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - +async fn check_dictation_config() -> Result, StatusCode> { let config = goose::config::Config::global(); // Check if ElevenLabs API key is configured @@ -410,10 +399,7 @@ mod tests { #[tokio::test] async fn test_transcribe_endpoint_requires_auth() { - let state = AppState::new( - Arc::new(goose::agents::Agent::new()), - "test-secret".to_string(), - ); + let state = AppState::new(Arc::new(goose::agents::Agent::new())).await; let app = routes(state); // Test without auth header @@ -436,10 +422,7 @@ mod tests { #[tokio::test] async fn test_transcribe_endpoint_validates_size() { - let state = AppState::new( - Arc::new(goose::agents::Agent::new()), - "test-secret".to_string(), - ); + let state = AppState::new(Arc::new(goose::agents::Agent::new())).await; let app = routes(state); // Create a large base64 string (simulating > 25MB audio) @@ -465,10 +448,7 @@ mod tests { #[tokio::test] async fn test_transcribe_endpoint_validates_mime_type() { - let state = AppState::new( - Arc::new(goose::agents::Agent::new()), - "test-secret".to_string(), - ); + let state = AppState::new(Arc::new(goose::agents::Agent::new())).await; let app = routes(state); let request = Request::builder() @@ -494,10 +474,7 @@ mod tests { #[tokio::test] async fn test_transcribe_endpoint_handles_invalid_base64() { - let state = AppState::new( - Arc::new(goose::agents::Agent::new()), - "test-secret".to_string(), - ); + let state = AppState::new(Arc::new(goose::agents::Agent::new())).await; let app = routes(state); let request = Request::builder() diff --git a/crates/goose-server/src/routes/config_management.rs b/crates/goose-server/src/routes/config_management.rs index 5c5b8bbb2f47..b5496bfb890c 100644 --- a/crates/goose-server/src/routes/config_management.rs +++ b/crates/goose-server/src/routes/config_management.rs @@ -1,4 +1,3 @@ -use super::utils::verify_secret_key; use crate::routes::utils::check_provider_configured; use crate::state::AppState; use axum::{ @@ -17,7 +16,7 @@ use goose::providers::pricing::{ }; use goose::providers::providers as get_providers; use goose::{agents::ExtensionConfig, config::permission::PermissionLevel}; -use http::{HeaderMap, StatusCode}; +use http::StatusCode; use serde::{Deserialize, Serialize}; use serde_json::Value; use serde_yaml; @@ -97,12 +96,8 @@ pub struct CreateCustomProviderRequest { ) )] pub async fn upsert_config( - State(state): State>, - headers: HeaderMap, Json(query): Json, ) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - let config = Config::global(); let result = config.set(&query.key, query.value, query.is_secret); @@ -122,13 +117,7 @@ pub async fn upsert_config( (status = 500, description = "Internal server error") ) )] -pub async fn remove_config( - State(state): State>, - headers: HeaderMap, - Json(query): Json, -) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - +pub async fn remove_config(Json(query): Json) -> Result, StatusCode> { let config = Config::global(); let result = if query.is_secret { @@ -152,13 +141,7 @@ pub async fn remove_config( (status = 500, description = "Unable to get the configuration value"), ) )] -pub async fn read_config( - State(state): State>, - headers: HeaderMap, - Json(query): Json, -) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - +pub async fn read_config(Json(query): Json) -> Result, StatusCode> { if query.key == "model-limits" { let limits = ModelConfig::get_all_model_limits(); return Ok(Json( @@ -198,12 +181,7 @@ pub async fn read_config( (status = 500, description = "Internal server error") ) )] -pub async fn get_extensions( - State(state): State>, - headers: HeaderMap, -) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - +pub async fn get_extensions() -> Result, StatusCode> { match ExtensionConfigManager::get_all() { Ok(extensions) => Ok(Json(ExtensionResponse { extensions })), Err(err) => { @@ -231,12 +209,8 @@ pub async fn get_extensions( ) )] pub async fn add_extension( - State(state): State>, - headers: HeaderMap, Json(extension_query): Json, ) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - let extensions = ExtensionConfigManager::get_all().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; let key = goose::config::extensions::name_to_key(&extension_query.name); @@ -268,12 +242,8 @@ pub async fn add_extension( ) )] pub async fn remove_extension( - State(state): State>, - headers: HeaderMap, axum::extract::Path(name): axum::extract::Path, ) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - let key = goose::config::extensions::name_to_key(&name); match ExtensionConfigManager::remove(&key) { Ok(_) => Ok(Json(format!("Removed extension {}", name))), @@ -288,12 +258,7 @@ pub async fn remove_extension( (status = 200, description = "All configuration values retrieved successfully", body = ConfigResponse) ) )] -pub async fn read_all_config( - State(state): State>, - headers: HeaderMap, -) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - +pub async fn read_all_config() -> Result, StatusCode> { let config = Config::global(); let values = config @@ -310,12 +275,7 @@ pub async fn read_all_config( (status = 200, description = "All configuration values retrieved successfully", body = [ProviderDetails]) ) )] -pub async fn providers( - State(state): State>, - headers: HeaderMap, -) -> Result>, StatusCode> { - verify_secret_key(&headers, &state)?; - +pub async fn providers() -> Result>, StatusCode> { let mut providers_metadata = get_providers(); let custom_providers_dir = goose::config::custom_providers::custom_providers_dir(); @@ -480,12 +440,8 @@ pub struct PricingQuery { ) )] pub async fn get_pricing( - State(state): State>, - headers: HeaderMap, Json(query): Json, ) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - let configured_only = query.configured_only.unwrap_or(true); // If refresh requested (configured_only = false), refresh the cache @@ -578,12 +534,7 @@ pub async fn get_pricing( (status = 500, description = "Internal server error") ) )] -pub async fn init_config( - State(state): State>, - headers: HeaderMap, -) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - +pub async fn init_config() -> Result, StatusCode> { let config = Config::global(); if config.exists() { @@ -612,12 +563,8 @@ pub async fn init_config( ) )] pub async fn upsert_permissions( - State(state): State>, - headers: HeaderMap, Json(query): Json, ) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - let mut permission_manager = goose::config::PermissionManager::default(); for tool_permission in &query.tool_permissions { @@ -638,12 +585,7 @@ pub async fn upsert_permissions( (status = 500, description = "Internal server error") ) )] -pub async fn backup_config( - State(state): State>, - headers: HeaderMap, -) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - +pub async fn backup_config() -> Result, StatusCode> { let config_dir = choose_app_strategy(APP_STRATEGY.clone()) .expect("goose requires a home dir") .config_dir(); @@ -676,12 +618,7 @@ pub async fn backup_config( (status = 500, description = "Internal server error") ) )] -pub async fn recover_config( - State(state): State>, - headers: HeaderMap, -) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - +pub async fn recover_config() -> Result, StatusCode> { let config = Config::global(); // Force a reload which will trigger recovery if needed @@ -713,12 +650,7 @@ pub async fn recover_config( (status = 422, description = "Config file is corrupted") ) )] -pub async fn validate_config( - State(state): State>, - headers: HeaderMap, -) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - +pub async fn validate_config() -> Result, StatusCode> { let config_dir = choose_app_strategy(APP_STRATEGY.clone()) .expect("goose requires a home dir") .config_dir(); @@ -751,12 +683,7 @@ pub async fn validate_config( (status = 200, description = "Current model retrieved successfully", body = String), ) )] -pub async fn get_current_model( - State(state): State>, - headers: HeaderMap, -) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - +pub async fn get_current_model() -> Result, StatusCode> { let current_model = goose::providers::base::get_current_model(); Ok(Json(serde_json::json!({ @@ -775,12 +702,8 @@ pub async fn get_current_model( ) )] pub async fn create_custom_provider( - State(state): State>, - headers: HeaderMap, Json(request): Json, ) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - let config = goose::config::custom_providers::CustomProviderConfig::create_and_save( &request.provider_type, request.display_name, @@ -808,12 +731,8 @@ pub async fn create_custom_provider( ) )] pub async fn remove_custom_provider( - State(state): State>, - headers: HeaderMap, axum::extract::Path(id): axum::extract::Path, ) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - goose::config::custom_providers::CustomProviderConfig::remove(&id) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; @@ -852,13 +771,13 @@ pub fn routes(state: Arc) -> Router { #[cfg(test)] mod tests { + use http::HeaderMap; + use super::*; - async fn create_test_state() -> Arc { - let test_state = AppState::new( - Arc::new(goose::agents::Agent::default()), - "test".to_string(), - ); + #[tokio::test] + async fn test_read_model_limits() { + let test_state = AppState::new(Arc::new(goose::agents::Agent::default())).await; let sched_storage_path = choose_app_strategy(APP_STRATEGY.clone()) .unwrap() .data_dir() @@ -876,14 +795,10 @@ mod tests { let mut headers = HeaderMap::new(); headers.insert("X-Secret-Key", "test".parse().unwrap()); - let result = read_config( - State(test_state), - headers, - Json(ConfigKeyQuery { - key: "model-limits".to_string(), - is_secret: false, - }), - ) + let result = read_config(Json(ConfigKeyQuery { + key: "model-limits".to_string(), + is_secret: false, + })) .await; assert!(result.is_ok()); diff --git a/crates/goose-server/src/routes/context.rs b/crates/goose-server/src/routes/context.rs index 7f23b8777fd3..0a1899252124 100644 --- a/crates/goose-server/src/routes/context.rs +++ b/crates/goose-server/src/routes/context.rs @@ -1,11 +1,5 @@ -use super::utils::verify_secret_key; use crate::state::AppState; -use axum::{ - extract::State, - http::{HeaderMap, StatusCode}, - routing::post, - Json, Router, -}; +use axum::{extract::State, http::StatusCode, routing::post, Json, Router}; use goose::conversation::{message::Message, Conversation}; use serde::{Deserialize, Serialize}; use std::sync::Arc; @@ -48,11 +42,8 @@ pub struct ContextManageResponse { )] async fn manage_context( State(state): State>, - headers: HeaderMap, Json(request): Json, ) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - let agent = state.get_agent().await; let mut processed_messages = Conversation::new_unvalidated(vec![]); diff --git a/crates/goose-server/src/routes/extension.rs b/crates/goose-server/src/routes/extension.rs index 1567fb71ecfc..8102373a6cdd 100644 --- a/crates/goose-server/src/routes/extension.rs +++ b/crates/goose-server/src/routes/extension.rs @@ -3,11 +3,10 @@ use std::path::Path; use std::sync::Arc; use std::sync::OnceLock; -use super::utils::verify_secret_key; use crate::state::AppState; use axum::{extract::State, routing::post, Json, Router}; use goose::agents::{extension::Envs, ExtensionConfig}; -use http::{HeaderMap, StatusCode}; +use http::StatusCode; use rmcp::model::Tool; use serde::{Deserialize, Serialize}; use tracing; @@ -100,11 +99,8 @@ struct ExtensionResponse { /// Handler for adding a new extension configuration. async fn add_extension( State(state): State>, - headers: HeaderMap, raw: axum::extract::Json, ) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - // Log the raw request for debugging tracing::info!( "Received extension request: {}", @@ -296,11 +292,8 @@ async fn add_extension( /// Handler for removing an extension by name async fn remove_extension( State(state): State>, - headers: HeaderMap, Json(name): Json, ) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - let agent = state.get_agent().await; match agent.remove_extension(&name).await { Ok(_) => Ok(Json(ExtensionResponse { diff --git a/crates/goose-server/src/routes/mod.rs b/crates/goose-server/src/routes/mod.rs index bc86bbd4ef08..65764143d563 100644 --- a/crates/goose-server/src/routes/mod.rs +++ b/crates/goose-server/src/routes/mod.rs @@ -1,4 +1,3 @@ -// Export route modules pub mod agent; pub mod audio; pub mod config_management; diff --git a/crates/goose-server/src/routes/reply.rs b/crates/goose-server/src/routes/reply.rs index a11ce3134501..34c4a8f60853 100644 --- a/crates/goose-server/src/routes/reply.rs +++ b/crates/goose-server/src/routes/reply.rs @@ -1,8 +1,7 @@ -use super::utils::verify_secret_key; use crate::state::AppState; use axum::{ extract::{DefaultBodyLimit, State}, - http::{self, HeaderMap, StatusCode}, + http::{self, StatusCode}, response::IntoResponse, routing::post, Json, Router, @@ -168,11 +167,8 @@ async fn stream_event( async fn reply_handler( State(state): State>, - headers: HeaderMap, Json(request): Json, ) -> Result { - verify_secret_key(&headers, &state)?; - let session_start = std::time::Instant::now(); tracing::info!( @@ -466,11 +462,8 @@ fn default_principal_type() -> PrincipalType { )] pub async fn confirm_permission( State(state): State>, - headers: HeaderMap, Json(request): Json, ) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - let agent = state.get_agent().await; let permission = match request.action.as_str() { "always_allow" => Permission::AlwaysAllow, @@ -501,11 +494,8 @@ struct ToolResultRequest { async fn submit_tool_result( State(state): State>, - headers: HeaderMap, raw: Json, ) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - tracing::info!( "Received tool result request: {}", serde_json::to_string_pretty(&raw.0).unwrap() @@ -599,7 +589,7 @@ mod tests { }); let agent = Agent::new(); let _ = agent.update_provider(mock_provider).await; - let state = AppState::new(Arc::new(agent), "test-secret".to_string()); + let state = AppState::new(Arc::new(agent)); let app = routes(state); diff --git a/crates/goose-server/src/routes/schedule.rs b/crates/goose-server/src/routes/schedule.rs index 7f6e58e4b35e..663890080089 100644 --- a/crates/goose-server/src/routes/schedule.rs +++ b/crates/goose-server/src/routes/schedule.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use axum::{ extract::{Path, Query, State}, - http::{HeaderMap, StatusCode}, + http::StatusCode, routing::{delete, get, post, put}, Json, Router, }; @@ -10,7 +10,6 @@ use serde::{Deserialize, Serialize}; use chrono::NaiveDateTime; -use crate::routes::utils::verify_secret_key; use crate::state::AppState; use goose::scheduler::ScheduledJob; @@ -104,10 +103,8 @@ fn parse_session_name_to_iso(session_name: &str) -> String { #[axum::debug_handler] async fn create_schedule( State(state): State>, - headers: HeaderMap, Json(req): Json, ) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; let scheduler = state .scheduler() .await @@ -156,9 +153,7 @@ async fn create_schedule( #[axum::debug_handler] async fn list_schedules( State(state): State>, - headers: HeaderMap, ) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; let scheduler = state .scheduler() .await @@ -188,10 +183,8 @@ async fn list_schedules( #[axum::debug_handler] async fn delete_schedule( State(state): State>, - headers: HeaderMap, Path(id): Path, ) -> Result { - verify_secret_key(&headers, &state)?; let scheduler = state .scheduler() .await @@ -222,10 +215,8 @@ async fn delete_schedule( #[axum::debug_handler] async fn run_now_handler( State(state): State>, - headers: HeaderMap, Path(id): Path, ) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; let scheduler = state .scheduler() .await @@ -315,11 +306,9 @@ async fn run_now_handler( #[axum::debug_handler] async fn sessions_handler( State(state): State>, - headers: HeaderMap, // Added this line Path(schedule_id_param): Path, // Renamed to avoid confusion with session_id Query(query_params): Query, ) -> Result>, StatusCode> { - verify_secret_key(&headers, &state)?; // Added this line let scheduler = state .scheduler() .await @@ -377,10 +366,8 @@ async fn sessions_handler( #[axum::debug_handler] async fn pause_schedule( State(state): State>, - headers: HeaderMap, Path(id): Path, ) -> Result { - verify_secret_key(&headers, &state)?; let scheduler = state .scheduler() .await @@ -413,10 +400,8 @@ async fn pause_schedule( #[axum::debug_handler] async fn unpause_schedule( State(state): State>, - headers: HeaderMap, Path(id): Path, ) -> Result { - verify_secret_key(&headers, &state)?; let scheduler = state .scheduler() .await @@ -450,11 +435,9 @@ async fn unpause_schedule( #[axum::debug_handler] async fn update_schedule( State(state): State>, - headers: HeaderMap, Path(id): Path, Json(req): Json, ) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; let scheduler = state .scheduler() .await @@ -497,10 +480,8 @@ async fn update_schedule( #[axum::debug_handler] pub async fn kill_running_job( State(state): State>, - headers: HeaderMap, Path(id): Path, ) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; let scheduler = state .scheduler() .await @@ -536,10 +517,8 @@ pub async fn kill_running_job( #[axum::debug_handler] pub async fn inspect_running_job( State(state): State>, - headers: HeaderMap, Path(id): Path, ) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; let scheduler = state .scheduler() .await diff --git a/crates/goose-server/src/routes/session.rs b/crates/goose-server/src/routes/session.rs index 432bbc398104..5debd7381fa4 100644 --- a/crates/goose-server/src/routes/session.rs +++ b/crates/goose-server/src/routes/session.rs @@ -1,12 +1,11 @@ -use super::utils::verify_secret_key; use chrono::DateTime; use std::collections::HashMap; use std::sync::Arc; use crate::state::AppState; use axum::{ - extract::{Path, State}, - http::{HeaderMap, StatusCode}, + extract::Path, + http::StatusCode, routing::{delete, get, put}, Json, Router, }; @@ -82,12 +81,7 @@ pub struct ActivityHeatmapCell { tag = "Session Management" )] // List all available sessions -async fn list_sessions( - State(state): State>, - headers: HeaderMap, -) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - +async fn list_sessions() -> Result, StatusCode> { let sessions = get_valid_sorted_sessions(SortOrder::Descending) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; @@ -113,12 +107,8 @@ async fn list_sessions( )] // Get a specific session's history async fn get_session_history( - State(state): State>, - headers: HeaderMap, Path(session_id): Path, ) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - let session_path = match session::get_path(session::Identifier::Name(session_id.clone())) { Ok(path) => path, Err(_) => return Err(StatusCode::BAD_REQUEST), @@ -154,14 +144,9 @@ async fn get_session_history( ), tag = "Session Management" )] -async fn get_session_insights( - State(state): State>, - headers: HeaderMap, -) -> Result, StatusCode> { +async fn get_session_insights() -> Result, StatusCode> { info!("Received request for session insights"); - verify_secret_key(&headers, &state)?; - let sessions = get_valid_sorted_sessions(SortOrder::Descending).map_err(|e| { error!("Failed to get session info: {:?}", e); StatusCode::INTERNAL_SERVER_ERROR @@ -281,13 +266,9 @@ async fn get_session_insights( )] // Update session metadata async fn update_session_metadata( - State(state): State>, - headers: HeaderMap, Path(session_id): Path, Json(request): Json, ) -> Result { - verify_secret_key(&headers, &state)?; - // Validate description length if request.description.len() > MAX_DESCRIPTION_LENGTH { return Err(StatusCode::BAD_REQUEST); diff --git a/crates/goose-server/src/routes/utils.rs b/crates/goose-server/src/routes/utils.rs index 94df7df2dc08..c1c32fd00668 100644 --- a/crates/goose-server/src/routes/utils.rs +++ b/crates/goose-server/src/routes/utils.rs @@ -1,7 +1,5 @@ -use crate::state::AppState; use goose::config::Config; use goose::providers::base::{ConfigKey, ProviderMetadata}; -use http::{HeaderMap, StatusCode}; use serde::{Deserialize, Serialize}; use std::env; use std::error::Error; @@ -23,20 +21,6 @@ pub struct KeyInfo { pub value: Option, // Only populated for non-secret keys that are set } -pub fn verify_secret_key(headers: &HeaderMap, state: &AppState) -> Result { - // Verify secret key - let secret_key = headers - .get("X-Secret-Key") - .and_then(|value| value.to_str().ok()) - .ok_or(StatusCode::UNAUTHORIZED)?; - - if secret_key != state.secret_key { - Err(StatusCode::UNAUTHORIZED) - } else { - Ok(StatusCode::OK) - } -} - /// Inspects a configuration key to determine if it's set, its location, and value (for non-secret keys) #[allow(dead_code)] pub fn inspect_key(key_name: &str, is_secret: bool) -> Result> { diff --git a/crates/goose-server/src/state.rs b/crates/goose-server/src/state.rs index eacbfaf25149..a70cd47296b5 100644 --- a/crates/goose-server/src/state.rs +++ b/crates/goose-server/src/state.rs @@ -12,17 +12,15 @@ type AgentRef = Arc; #[derive(Clone)] pub struct AppState { agent: Arc>, - pub secret_key: String, pub scheduler: Arc>>>, pub recipe_file_hash_map: Arc>>, pub session_counter: Arc, } impl AppState { - pub fn new(agent: AgentRef, secret_key: String) -> Arc { + pub fn new(agent: AgentRef) -> Arc { Arc::new(Self { agent: Arc::new(RwLock::new(agent)), - secret_key, scheduler: Arc::new(RwLock::new(None)), recipe_file_hash_map: Arc::new(Mutex::new(HashMap::new())), session_counter: Arc::new(AtomicUsize::new(0)), diff --git a/crates/goose-server/tests/pricing_api_test.rs b/crates/goose-server/tests/pricing_api_test.rs index d7b48b2a5910..456710c2fa53 100644 --- a/crates/goose-server/tests/pricing_api_test.rs +++ b/crates/goose-server/tests/pricing_api_test.rs @@ -8,7 +8,7 @@ use tower::ServiceExt; async fn create_test_app() -> Router { let agent = Arc::new(goose::agents::Agent::default()); - let state = goose_server::AppState::new(agent, "test".to_string()); + let state = goose_server::AppState::new(agent); // Add scheduler setup like in the existing tests let sched_storage_path = etcetera::choose_app_strategy(goose::config::APP_STRATEGY.clone()) diff --git a/ui/desktop/src/goosed.ts b/ui/desktop/src/goosed.ts index 67cad4539519..935c67374c46 100644 --- a/ui/desktop/src/goosed.ts +++ b/ui/desktop/src/goosed.ts @@ -25,25 +25,24 @@ export const findAvailablePort = (): Promise => { // Goose process manager. Take in the app, port, and directory to start goosed in. // Check if goosed server is ready by polling the status endpoint -const checkServerStatus = async ( - port: number, - maxAttempts?: number, - interval: number = 100 -): Promise => { - if (maxAttempts === undefined) { - const isTemporalEnabled = process.env.GOOSE_SCHEDULER_TYPE === 'temporal'; - maxAttempts = isTemporalEnabled ? 200 : 80; - log.info( - `Using ${maxAttempts} max attempts (temporal scheduling: ${isTemporalEnabled ? 'enabled' : 'disabled'})` - ); - } +const checkServerStatus = async (port: number, secret: string): Promise => { + const isTemporalEnabled = process.env.GOOSE_SCHEDULER_TYPE === 'temporal'; + const maxAttempts = isTemporalEnabled ? 200 : 80; + const interval = 100; + log.info( + `Using ${maxAttempts} max attempts (temporal scheduling: ${isTemporalEnabled ? 'enabled' : 'disabled'})` + ); const statusUrl = `http://127.0.0.1:${port}/status`; log.info(`Checking server status at ${statusUrl}`); for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { - const response = await fetch(statusUrl); + const response = await fetch(statusUrl, { + headers: { + 'X-Secret-Key': secret, + }, + }); if (response.ok) { log.info(`Server is ready after ${attempt} attempts`); return true; @@ -65,7 +64,7 @@ const connectToExternalBackend = async ( ): Promise<[number, string, ChildProcess]> => { log.info(`Using external goosed backend on port ${port}`); - const isReady = await checkServerStatus(port); + const isReady = await checkServerStatus(port, 'test'); if (!isReady) { throw new Error(`External goosed server not accessible on port ${port}`); } @@ -267,7 +266,7 @@ export const startGoosed = async ( }); // Wait for the server to be ready - const isReady = await checkServerStatus(port); + const isReady = await checkServerStatus(port, serverSecret); log.info(`Goosed isReady ${isReady}`); const try_kill_goose = () => { From bb7ea9080ba412ca0ab7ffdd38e12910e115906d Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Tue, 26 Aug 2025 11:52:59 -0400 Subject: [PATCH 2/8] WIP api clients --- crates/goose-server/src/auth.rs | 2 +- crates/goose-server/src/commands/agent.rs | 7 +++-- crates/goose-server/src/openapi.rs | 1 + crates/goose-server/src/routes/health.rs | 19 ++++++------- ui/desktop/openapi.json | 20 +++++++++++++ ui/desktop/src/api/index.ts | 11 +++++++- ui/desktop/src/api/sdk.gen.ts | 34 ++++++----------------- ui/desktop/src/api/types.gen.ts | 16 +++++++++++ ui/desktop/src/goosed.ts | 15 ++++------ ui/desktop/src/main.ts | 2 +- ui/desktop/src/renderer.tsx | 3 -- ui/desktop/src/utils.ts | 7 +---- 12 files changed, 76 insertions(+), 61 deletions(-) diff --git a/crates/goose-server/src/auth.rs b/crates/goose-server/src/auth.rs index 3c6dd6602e46..ba13f59b5eee 100644 --- a/crates/goose-server/src/auth.rs +++ b/crates/goose-server/src/auth.rs @@ -5,7 +5,7 @@ use axum::{ response::Response, }; -pub async fn layer_fn( +pub async fn check_token( State(state): State, request: Request, next: Next, diff --git a/crates/goose-server/src/commands/agent.rs b/crates/goose-server/src/commands/agent.rs index d300cef5a4ee..c88d99632cbc 100644 --- a/crates/goose-server/src/commands/agent.rs +++ b/crates/goose-server/src/commands/agent.rs @@ -8,7 +8,7 @@ use etcetera::{choose_app_strategy, AppStrategy}; use goose::agents::Agent; use goose::config::APP_STRATEGY; use goose::scheduler_factory::SchedulerFactory; -use goose_server::auth::layer_fn; +use goose_server::auth::check_token; use tower_http::cors::{Any, CorsLayer}; use tracing::info; @@ -53,7 +53,10 @@ pub async fn run() -> Result<()> { .allow_headers(Any); let app = crate::routes::configure(app_state) - .layer(middleware::from_fn_with_state(secret_key.clone(), layer_fn)) + .layer(middleware::from_fn_with_state( + secret_key.clone(), + check_token, + )) .layer(cors); let listener = tokio::net::TcpListener::bind(settings.socket_addr()).await?; diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index d78746f4d775..295af9f2ac66 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -355,6 +355,7 @@ impl<'__s> ToSchema<'__s> for AnnotatedSchema { #[derive(OpenApi)] #[openapi( paths( + super::routes::health::status, super::routes::config_management::backup_config, super::routes::config_management::recover_config, super::routes::config_management::validate_config, diff --git a/crates/goose-server/src/routes/health.rs b/crates/goose-server/src/routes/health.rs index aeed1692b983..137230240255 100644 --- a/crates/goose-server/src/routes/health.rs +++ b/crates/goose-server/src/routes/health.rs @@ -1,17 +1,14 @@ -use axum::{routing::get, Json, Router}; -use serde::Serialize; +use axum::{routing::get, Router}; -#[derive(Serialize)] -struct StatusResponse { - status: &'static str, +#[utoipa::path(get, path = "/status", + responses( + (status = 200, description = "ok", body = String), + ) +)] +async fn status() -> String { + "ok".to_string() } -/// Simple status endpoint that returns 200 OK when the server is running -async fn status() -> Json { - Json(StatusResponse { status: "ok" }) -} - -/// Configure health check routes pub fn routes() -> Router { Router::new().route("/status", get(status)) } diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index ad35b702dbbe..92ed5c4edbc5 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -1504,6 +1504,26 @@ } ] } + }, + "/status": { + "get": { + "tags": [ + "super::routes::health" + ], + "operationId": "status", + "responses": { + "200": { + "description": "ok", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } } }, "components": { diff --git a/ui/desktop/src/api/index.ts b/ui/desktop/src/api/index.ts index e64537d21293..9f4ce672a057 100644 --- a/ui/desktop/src/api/index.ts +++ b/ui/desktop/src/api/index.ts @@ -1,3 +1,12 @@ // This file is auto-generated by @hey-api/openapi-ts export * from './types.gen'; -export * from './sdk.gen'; \ No newline at end of file +export * from './sdk.gen'; + +import {client} from './client.gen'; + +function configureRequest(request: Request) { + request.headers.append('X-Secret-Key', ''); + return request; +} + +client.interceptors.request.use(configureRequest) \ No newline at end of file diff --git a/ui/desktop/src/api/sdk.gen.ts b/ui/desktop/src/api/sdk.gen.ts index 1e6425e11929..5753708f12e2 100644 --- a/ui/desktop/src/api/sdk.gen.ts +++ b/ui/desktop/src/api/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import type { Options as ClientOptions, TDataShape, Client } from './client'; -import type { AddSubRecipesData, AddSubRecipesResponses, AddSubRecipesErrors, ExtendPromptData, ExtendPromptResponses, ExtendPromptErrors, ResumeAgentData, ResumeAgentResponses, ResumeAgentErrors, UpdateSessionConfigData, UpdateSessionConfigResponses, UpdateSessionConfigErrors, StartAgentData, StartAgentResponses, StartAgentErrors, GetToolsData, GetToolsResponses, GetToolsErrors, UpdateAgentProviderData, UpdateAgentProviderResponses, UpdateAgentProviderErrors, UpdateRouterToolSelectorData, UpdateRouterToolSelectorResponses, UpdateRouterToolSelectorErrors, ReadAllConfigData, ReadAllConfigResponses, BackupConfigData, BackupConfigResponses, BackupConfigErrors, CreateCustomProviderData, CreateCustomProviderResponses, CreateCustomProviderErrors, RemoveCustomProviderData, RemoveCustomProviderResponses, RemoveCustomProviderErrors, GetExtensionsData, GetExtensionsResponses, GetExtensionsErrors, AddExtensionData, AddExtensionResponses, AddExtensionErrors, RemoveExtensionData, RemoveExtensionResponses, RemoveExtensionErrors, InitConfigData, InitConfigResponses, InitConfigErrors, UpsertPermissionsData, UpsertPermissionsResponses, UpsertPermissionsErrors, ProvidersData, ProvidersResponses, GetProviderModelsData, GetProviderModelsResponses, GetProviderModelsErrors, ReadConfigData, ReadConfigResponses, ReadConfigErrors, RecoverConfigData, RecoverConfigResponses, RecoverConfigErrors, RemoveConfigData, RemoveConfigResponses, RemoveConfigErrors, UpsertConfigData, UpsertConfigResponses, UpsertConfigErrors, ValidateConfigData, ValidateConfigResponses, ValidateConfigErrors, ConfirmPermissionData, ConfirmPermissionResponses, ConfirmPermissionErrors, ManageContextData, ManageContextResponses, ManageContextErrors, CreateRecipeData, CreateRecipeResponses, CreateRecipeErrors, DecodeRecipeData, DecodeRecipeResponses, DecodeRecipeErrors, DeleteRecipeData, DeleteRecipeResponses, DeleteRecipeErrors, EncodeRecipeData, EncodeRecipeResponses, EncodeRecipeErrors, ListRecipesData, ListRecipesResponses, ListRecipesErrors, ScanRecipeData, ScanRecipeResponses, CreateScheduleData, CreateScheduleResponses, CreateScheduleErrors, DeleteScheduleData, DeleteScheduleResponses, DeleteScheduleErrors, ListSchedulesData, ListSchedulesResponses, ListSchedulesErrors, UpdateScheduleData, UpdateScheduleResponses, UpdateScheduleErrors, InspectRunningJobData, InspectRunningJobResponses, InspectRunningJobErrors, KillRunningJobData, KillRunningJobResponses, PauseScheduleData, PauseScheduleResponses, PauseScheduleErrors, RunNowHandlerData, RunNowHandlerResponses, RunNowHandlerErrors, SessionsHandlerData, SessionsHandlerResponses, SessionsHandlerErrors, UnpauseScheduleData, UnpauseScheduleResponses, UnpauseScheduleErrors, ListSessionsData, ListSessionsResponses, ListSessionsErrors, GetSessionHistoryData, GetSessionHistoryResponses, GetSessionHistoryErrors } from './types.gen'; +import type { AddSubRecipesData, AddSubRecipesResponses, AddSubRecipesErrors, ExtendPromptData, ExtendPromptResponses, ExtendPromptErrors, ResumeAgentData, ResumeAgentResponses, ResumeAgentErrors, UpdateSessionConfigData, UpdateSessionConfigResponses, UpdateSessionConfigErrors, StartAgentData, StartAgentResponses, StartAgentErrors, GetToolsData, GetToolsResponses, GetToolsErrors, UpdateAgentProviderData, UpdateAgentProviderResponses, UpdateAgentProviderErrors, UpdateRouterToolSelectorData, UpdateRouterToolSelectorResponses, UpdateRouterToolSelectorErrors, ReadAllConfigData, ReadAllConfigResponses, BackupConfigData, BackupConfigResponses, BackupConfigErrors, CreateCustomProviderData, CreateCustomProviderResponses, CreateCustomProviderErrors, RemoveCustomProviderData, RemoveCustomProviderResponses, RemoveCustomProviderErrors, GetExtensionsData, GetExtensionsResponses, GetExtensionsErrors, AddExtensionData, AddExtensionResponses, AddExtensionErrors, RemoveExtensionData, RemoveExtensionResponses, RemoveExtensionErrors, InitConfigData, InitConfigResponses, InitConfigErrors, UpsertPermissionsData, UpsertPermissionsResponses, UpsertPermissionsErrors, ProvidersData, ProvidersResponses, ReadConfigData, ReadConfigResponses, ReadConfigErrors, RecoverConfigData, RecoverConfigResponses, RecoverConfigErrors, RemoveConfigData, RemoveConfigResponses, RemoveConfigErrors, UpsertConfigData, UpsertConfigResponses, UpsertConfigErrors, ValidateConfigData, ValidateConfigResponses, ValidateConfigErrors, ConfirmPermissionData, ConfirmPermissionResponses, ConfirmPermissionErrors, ManageContextData, ManageContextResponses, ManageContextErrors, CreateRecipeData, CreateRecipeResponses, CreateRecipeErrors, DecodeRecipeData, DecodeRecipeResponses, DecodeRecipeErrors, EncodeRecipeData, EncodeRecipeResponses, EncodeRecipeErrors, ScanRecipeData, ScanRecipeResponses, CreateScheduleData, CreateScheduleResponses, CreateScheduleErrors, DeleteScheduleData, DeleteScheduleResponses, DeleteScheduleErrors, ListSchedulesData, ListSchedulesResponses, ListSchedulesErrors, UpdateScheduleData, UpdateScheduleResponses, UpdateScheduleErrors, InspectRunningJobData, InspectRunningJobResponses, InspectRunningJobErrors, KillRunningJobData, KillRunningJobResponses, PauseScheduleData, PauseScheduleResponses, PauseScheduleErrors, RunNowHandlerData, RunNowHandlerResponses, RunNowHandlerErrors, SessionsHandlerData, SessionsHandlerResponses, SessionsHandlerErrors, UnpauseScheduleData, UnpauseScheduleResponses, UnpauseScheduleErrors, ListSessionsData, ListSessionsResponses, ListSessionsErrors, GetSessionHistoryData, GetSessionHistoryResponses, GetSessionHistoryErrors, StatusData, StatusResponses } from './types.gen'; import { client as _heyApiClient } from './client.gen'; export type Options = ClientOptions & { @@ -184,13 +184,6 @@ export const providers = (options?: Option }); }; -export const getProviderModels = (options: Options) => { - return (options.client ?? _heyApiClient).get({ - url: '/config/providers/{name}/models', - ...options - }); -}; - export const readConfig = (options: Options) => { return (options.client ?? _heyApiClient).post({ url: '/config/read', @@ -285,17 +278,6 @@ export const decodeRecipe = (options: Opti }); }; -export const deleteRecipe = (options: Options) => { - return (options.client ?? _heyApiClient).post({ - url: '/recipes/delete', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } - }); -}; - export const encodeRecipe = (options: Options) => { return (options.client ?? _heyApiClient).post({ url: '/recipes/encode', @@ -307,13 +289,6 @@ export const encodeRecipe = (options: Opti }); }; -export const listRecipes = (options?: Options) => { - return (options?.client ?? _heyApiClient).get({ - url: '/recipes/list', - ...options - }); -}; - export const scanRecipe = (options: Options) => { return (options.client ?? _heyApiClient).post({ url: '/recipes/scan', @@ -415,4 +390,11 @@ export const getSessionHistory = (options: url: '/sessions/{session_id}', ...options }); +}; + +export const status = (options?: Options) => { + return (options?.client ?? _heyApiClient).get({ + url: '/status', + ...options + }); }; \ No newline at end of file diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 6fdcc63854c2..c5648ccaf9e7 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -2129,6 +2129,22 @@ export type GetSessionHistoryResponses = { export type GetSessionHistoryResponse = GetSessionHistoryResponses[keyof GetSessionHistoryResponses]; +export type StatusData = { + body?: never; + path?: never; + query?: never; + url: '/status'; +}; + +export type StatusResponses = { + /** + * ok + */ + 200: string; +}; + +export type StatusResponse = StatusResponses[keyof StatusResponses]; + export type ClientOptions = { baseUrl: `${string}://${string}` | (string & {}); }; \ No newline at end of file diff --git a/ui/desktop/src/goosed.ts b/ui/desktop/src/goosed.ts index 935c67374c46..3830bace8632 100644 --- a/ui/desktop/src/goosed.ts +++ b/ui/desktop/src/goosed.ts @@ -8,6 +8,9 @@ import log from './utils/logger'; import { App } from 'electron'; import { Buffer } from 'node:buffer'; +import {status} from "./api"; +import { client } from './api/client.gen'; + // Find an available port to start goosed on export const findAvailablePort = (): Promise => { return new Promise((resolve, _reject) => { @@ -35,20 +38,12 @@ const checkServerStatus = async (port: number, secret: string): Promise const statusUrl = `http://127.0.0.1:${port}/status`; log.info(`Checking server status at ${statusUrl}`); + log.info(`Client: ${JSON.stringify(client.getConfig(), undefined, 2)}`); for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { - const response = await fetch(statusUrl, { - headers: { - 'X-Secret-Key': secret, - }, - }); - if (response.ok) { - log.info(`Server is ready after ${attempt} attempts`); - return true; - } + await status({throwOnError: true}); } catch { - // Expected error when server isn't ready yet if (attempt === maxAttempts) { log.error(`Server failed to respond after ${maxAttempts} attempts`); } diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index 2a81088dc24e..dd3b986ad766 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -610,7 +610,7 @@ const createChat = async ( contextIsolation: true, additionalArguments: [ JSON.stringify({ - ...appConfig, // Use the potentially updated appConfig + ...appConfig, GOOSE_PORT: port, GOOSE_WORKING_DIR: working_dir, REQUEST_DIR: dir, diff --git a/ui/desktop/src/renderer.tsx b/ui/desktop/src/renderer.tsx index 8f8694e82e03..9f3fd6052f42 100644 --- a/ui/desktop/src/renderer.tsx +++ b/ui/desktop/src/renderer.tsx @@ -6,11 +6,8 @@ import { RequireClientInitialization, } from './contexts/ClientInitializationContext'; import { ErrorBoundary } from './components/ErrorBoundary'; -import { patchConsoleLogging } from './utils'; import SuspenseLoader from './suspense-loader'; -patchConsoleLogging(); - const App = lazy(() => import('./App')); ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/ui/desktop/src/utils.ts b/ui/desktop/src/utils.ts index 922519dd7b09..7378ae768234 100644 --- a/ui/desktop/src/utils.ts +++ b/ui/desktop/src/utils.ts @@ -10,9 +10,4 @@ export function snakeToTitleCase(snake: string): string { .split('_') .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(' '); -} - -export function patchConsoleLogging() { - // Intercept console methods - return; -} +} \ No newline at end of file From eb877c59d257add0f561c0955ee3f267a1ba8d63 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Mon, 8 Sep 2025 11:32:17 -0400 Subject: [PATCH 3/8] Fix bad resolves --- crates/goose-server/src/routes/audio.rs | 16 ++++----- .../src/routes/config_management.rs | 33 ++----------------- crates/goose-server/src/routes/recipe.rs | 9 ----- crates/goose-server/src/routes/session.rs | 8 +---- 4 files changed, 12 insertions(+), 54 deletions(-) diff --git a/crates/goose-server/src/routes/audio.rs b/crates/goose-server/src/routes/audio.rs index fefddc54e446..642ff88eee63 100644 --- a/crates/goose-server/src/routes/audio.rs +++ b/crates/goose-server/src/routes/audio.rs @@ -4,11 +4,13 @@ /// The OpenAI API key must be configured in the backend for this to work. use crate::state::AppState; use axum::{ + extract::State, http::StatusCode, routing::{get, post}, Json, Router, }; use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; +use http::HeaderMap; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::sync::Arc; @@ -207,12 +209,10 @@ async fn send_openai_request( /// - 502: Bad Gateway (OpenAI API error) /// - 503: Service Unavailable (network error) async fn transcribe_handler( - State(state): State>, - headers: HeaderMap, + _state: State>, + _headers: HeaderMap, Json(request): Json, ) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - let (audio_bytes, file_extension) = validate_audio_input(&request.audio, &request.mime_type)?; let (api_key, openai_host) = get_openai_config()?; @@ -399,7 +399,7 @@ mod tests { #[tokio::test] async fn test_transcribe_endpoint_requires_auth() { - let state = AppState::new(Arc::new(goose::agents::Agent::new())).await; + let state = AppState::new(Arc::new(goose::agents::Agent::new())); let app = routes(state); // Test without auth header @@ -422,7 +422,7 @@ mod tests { #[tokio::test] async fn test_transcribe_endpoint_validates_size() { - let state = AppState::new(Arc::new(goose::agents::Agent::new())).await; + let state = AppState::new(Arc::new(goose::agents::Agent::new())); let app = routes(state); // Create a large base64 string (simulating > 25MB audio) @@ -448,7 +448,7 @@ mod tests { #[tokio::test] async fn test_transcribe_endpoint_validates_mime_type() { - let state = AppState::new(Arc::new(goose::agents::Agent::new())).await; + let state = AppState::new(Arc::new(goose::agents::Agent::new())); let app = routes(state); let request = Request::builder() @@ -474,7 +474,7 @@ mod tests { #[tokio::test] async fn test_transcribe_endpoint_handles_invalid_base64() { - let state = AppState::new(Arc::new(goose::agents::Agent::new())).await; + let state = AppState::new(Arc::new(goose::agents::Agent::new())); let app = routes(state); let request = Request::builder() diff --git a/crates/goose-server/src/routes/config_management.rs b/crates/goose-server/src/routes/config_management.rs index b5496bfb890c..a461dc9be13f 100644 --- a/crates/goose-server/src/routes/config_management.rs +++ b/crates/goose-server/src/routes/config_management.rs @@ -1,7 +1,7 @@ use crate::routes::utils::check_provider_configured; use crate::state::AppState; use axum::{ - extract::{Path, State}, + extract::Path, routing::{delete, get, post}, Json, Router, }; @@ -363,12 +363,8 @@ pub async fn providers() -> Result>, StatusCode> { ) )] pub async fn get_provider_models( - State(state): State>, - headers: HeaderMap, Path(name): Path, ) -> Result>, StatusCode> { - verify_secret_key(&headers, &state)?; - let all = get_providers(); let Some(metadata) = all.into_iter().find(|m| m.name == name) else { return Err(StatusCode::BAD_REQUEST); @@ -777,21 +773,6 @@ mod tests { #[tokio::test] async fn test_read_model_limits() { - let test_state = AppState::new(Arc::new(goose::agents::Agent::default())).await; - let sched_storage_path = choose_app_strategy(APP_STRATEGY.clone()) - .unwrap() - .data_dir() - .join("schedules.json"); - let sched = goose::scheduler_factory::SchedulerFactory::create_legacy(sched_storage_path) - .await - .unwrap(); - test_state.set_scheduler(sched).await; - test_state - } - - #[tokio::test] - async fn test_read_model_limits() { - let test_state = create_test_state().await; let mut headers = HeaderMap::new(); headers.insert("X-Secret-Key", "test".parse().unwrap()); @@ -815,16 +796,10 @@ mod tests { #[tokio::test] async fn test_get_provider_models_unknown_provider() { - let test_state = create_test_state().await; let mut headers = HeaderMap::new(); headers.insert("X-Secret-Key", "test".parse().unwrap()); - let result = get_provider_models( - State(test_state), - headers, - Path("unknown_provider".to_string()), - ) - .await; + let result = get_provider_models(Path("unknown_provider".to_string())).await; assert!(result.is_err()); assert_eq!(result.unwrap_err(), StatusCode::BAD_REQUEST); @@ -834,12 +809,10 @@ mod tests { async fn test_get_provider_models_openai_configured() { std::env::set_var("OPENAI_API_KEY", "test-key"); - let test_state = create_test_state().await; let mut headers = HeaderMap::new(); headers.insert("X-Secret-Key", "test".parse().unwrap()); - let result = - get_provider_models(State(test_state), headers, Path("openai".to_string())).await; + let result = get_provider_models(Path("openai".to_string())).await; // The response should be BAD_REQUEST since the API key is invalid (authentication error) assert!( diff --git a/crates/goose-server/src/routes/recipe.rs b/crates/goose-server/src/routes/recipe.rs index 84b114b82f7d..5da49941113d 100644 --- a/crates/goose-server/src/routes/recipe.rs +++ b/crates/goose-server/src/routes/recipe.rs @@ -8,12 +8,10 @@ use goose::conversation::{message::Message, Conversation}; use goose::recipe::Recipe; use goose::recipe_deeplink; -use http::HeaderMap; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use crate::routes::recipe_utils::get_all_recipes_manifests; -use crate::routes::utils::verify_secret_key; use crate::state::AppState; #[derive(Debug, Deserialize, ToSchema)] @@ -230,10 +228,7 @@ async fn scan_recipe( )] async fn list_recipes( State(state): State>, - headers: HeaderMap, ) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - let recipe_manifest_with_paths = get_all_recipes_manifests().unwrap(); let mut recipe_file_hash_map = HashMap::new(); let recipe_manifest_responses = recipe_manifest_with_paths @@ -272,12 +267,8 @@ async fn list_recipes( )] async fn delete_recipe( State(state): State>, - headers: HeaderMap, Json(request): Json, ) -> StatusCode { - if verify_secret_key(&headers, &state).is_err() { - return StatusCode::UNAUTHORIZED; - } let recipe_file_hash_map = state.recipe_file_hash_map.lock().await; let file_path = match recipe_file_hash_map.get(&request.id) { Some(path) => path, diff --git a/crates/goose-server/src/routes/session.rs b/crates/goose-server/src/routes/session.rs index 5debd7381fa4..c3cd61c8778b 100644 --- a/crates/goose-server/src/routes/session.rs +++ b/crates/goose-server/src/routes/session.rs @@ -309,13 +309,7 @@ async fn update_session_metadata( tag = "Session Management" )] // Delete a session -async fn delete_session( - State(state): State>, - headers: HeaderMap, - Path(session_id): Path, -) -> Result { - verify_secret_key(&headers, &state)?; - +async fn delete_session(Path(session_id): Path) -> Result { // Get the session path let session_path = match session::get_path(session::Identifier::Name(session_id.clone())) { Ok(path) => path, From fda12f7674205209d2e251fef37124631d745c73 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Mon, 8 Sep 2025 11:33:57 -0400 Subject: [PATCH 4/8] eslint --- ui/desktop/src/api/index.ts | 11 +---------- ui/desktop/src/api/sdk.gen.ts | 27 ++++++++++++++++++++++++++- ui/desktop/src/goosed.ts | 10 +++++----- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/ui/desktop/src/api/index.ts b/ui/desktop/src/api/index.ts index 9f4ce672a057..e64537d21293 100644 --- a/ui/desktop/src/api/index.ts +++ b/ui/desktop/src/api/index.ts @@ -1,12 +1,3 @@ // This file is auto-generated by @hey-api/openapi-ts export * from './types.gen'; -export * from './sdk.gen'; - -import {client} from './client.gen'; - -function configureRequest(request: Request) { - request.headers.append('X-Secret-Key', ''); - return request; -} - -client.interceptors.request.use(configureRequest) \ No newline at end of file +export * from './sdk.gen'; \ No newline at end of file diff --git a/ui/desktop/src/api/sdk.gen.ts b/ui/desktop/src/api/sdk.gen.ts index 5753708f12e2..20dbc7797aae 100644 --- a/ui/desktop/src/api/sdk.gen.ts +++ b/ui/desktop/src/api/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import type { Options as ClientOptions, TDataShape, Client } from './client'; -import type { AddSubRecipesData, AddSubRecipesResponses, AddSubRecipesErrors, ExtendPromptData, ExtendPromptResponses, ExtendPromptErrors, ResumeAgentData, ResumeAgentResponses, ResumeAgentErrors, UpdateSessionConfigData, UpdateSessionConfigResponses, UpdateSessionConfigErrors, StartAgentData, StartAgentResponses, StartAgentErrors, GetToolsData, GetToolsResponses, GetToolsErrors, UpdateAgentProviderData, UpdateAgentProviderResponses, UpdateAgentProviderErrors, UpdateRouterToolSelectorData, UpdateRouterToolSelectorResponses, UpdateRouterToolSelectorErrors, ReadAllConfigData, ReadAllConfigResponses, BackupConfigData, BackupConfigResponses, BackupConfigErrors, CreateCustomProviderData, CreateCustomProviderResponses, CreateCustomProviderErrors, RemoveCustomProviderData, RemoveCustomProviderResponses, RemoveCustomProviderErrors, GetExtensionsData, GetExtensionsResponses, GetExtensionsErrors, AddExtensionData, AddExtensionResponses, AddExtensionErrors, RemoveExtensionData, RemoveExtensionResponses, RemoveExtensionErrors, InitConfigData, InitConfigResponses, InitConfigErrors, UpsertPermissionsData, UpsertPermissionsResponses, UpsertPermissionsErrors, ProvidersData, ProvidersResponses, ReadConfigData, ReadConfigResponses, ReadConfigErrors, RecoverConfigData, RecoverConfigResponses, RecoverConfigErrors, RemoveConfigData, RemoveConfigResponses, RemoveConfigErrors, UpsertConfigData, UpsertConfigResponses, UpsertConfigErrors, ValidateConfigData, ValidateConfigResponses, ValidateConfigErrors, ConfirmPermissionData, ConfirmPermissionResponses, ConfirmPermissionErrors, ManageContextData, ManageContextResponses, ManageContextErrors, CreateRecipeData, CreateRecipeResponses, CreateRecipeErrors, DecodeRecipeData, DecodeRecipeResponses, DecodeRecipeErrors, EncodeRecipeData, EncodeRecipeResponses, EncodeRecipeErrors, ScanRecipeData, ScanRecipeResponses, CreateScheduleData, CreateScheduleResponses, CreateScheduleErrors, DeleteScheduleData, DeleteScheduleResponses, DeleteScheduleErrors, ListSchedulesData, ListSchedulesResponses, ListSchedulesErrors, UpdateScheduleData, UpdateScheduleResponses, UpdateScheduleErrors, InspectRunningJobData, InspectRunningJobResponses, InspectRunningJobErrors, KillRunningJobData, KillRunningJobResponses, PauseScheduleData, PauseScheduleResponses, PauseScheduleErrors, RunNowHandlerData, RunNowHandlerResponses, RunNowHandlerErrors, SessionsHandlerData, SessionsHandlerResponses, SessionsHandlerErrors, UnpauseScheduleData, UnpauseScheduleResponses, UnpauseScheduleErrors, ListSessionsData, ListSessionsResponses, ListSessionsErrors, GetSessionHistoryData, GetSessionHistoryResponses, GetSessionHistoryErrors, StatusData, StatusResponses } from './types.gen'; +import type { AddSubRecipesData, AddSubRecipesResponses, AddSubRecipesErrors, ExtendPromptData, ExtendPromptResponses, ExtendPromptErrors, ResumeAgentData, ResumeAgentResponses, ResumeAgentErrors, UpdateSessionConfigData, UpdateSessionConfigResponses, UpdateSessionConfigErrors, StartAgentData, StartAgentResponses, StartAgentErrors, GetToolsData, GetToolsResponses, GetToolsErrors, UpdateAgentProviderData, UpdateAgentProviderResponses, UpdateAgentProviderErrors, UpdateRouterToolSelectorData, UpdateRouterToolSelectorResponses, UpdateRouterToolSelectorErrors, ReadAllConfigData, ReadAllConfigResponses, BackupConfigData, BackupConfigResponses, BackupConfigErrors, CreateCustomProviderData, CreateCustomProviderResponses, CreateCustomProviderErrors, RemoveCustomProviderData, RemoveCustomProviderResponses, RemoveCustomProviderErrors, GetExtensionsData, GetExtensionsResponses, GetExtensionsErrors, AddExtensionData, AddExtensionResponses, AddExtensionErrors, RemoveExtensionData, RemoveExtensionResponses, RemoveExtensionErrors, InitConfigData, InitConfigResponses, InitConfigErrors, UpsertPermissionsData, UpsertPermissionsResponses, UpsertPermissionsErrors, ProvidersData, ProvidersResponses, GetProviderModelsData, GetProviderModelsResponses, GetProviderModelsErrors, ReadConfigData, ReadConfigResponses, ReadConfigErrors, RecoverConfigData, RecoverConfigResponses, RecoverConfigErrors, RemoveConfigData, RemoveConfigResponses, RemoveConfigErrors, UpsertConfigData, UpsertConfigResponses, UpsertConfigErrors, ValidateConfigData, ValidateConfigResponses, ValidateConfigErrors, ConfirmPermissionData, ConfirmPermissionResponses, ConfirmPermissionErrors, ManageContextData, ManageContextResponses, ManageContextErrors, CreateRecipeData, CreateRecipeResponses, CreateRecipeErrors, DecodeRecipeData, DecodeRecipeResponses, DecodeRecipeErrors, DeleteRecipeData, DeleteRecipeResponses, DeleteRecipeErrors, EncodeRecipeData, EncodeRecipeResponses, EncodeRecipeErrors, ListRecipesData, ListRecipesResponses, ListRecipesErrors, ScanRecipeData, ScanRecipeResponses, CreateScheduleData, CreateScheduleResponses, CreateScheduleErrors, DeleteScheduleData, DeleteScheduleResponses, DeleteScheduleErrors, ListSchedulesData, ListSchedulesResponses, ListSchedulesErrors, UpdateScheduleData, UpdateScheduleResponses, UpdateScheduleErrors, InspectRunningJobData, InspectRunningJobResponses, InspectRunningJobErrors, KillRunningJobData, KillRunningJobResponses, PauseScheduleData, PauseScheduleResponses, PauseScheduleErrors, RunNowHandlerData, RunNowHandlerResponses, RunNowHandlerErrors, SessionsHandlerData, SessionsHandlerResponses, SessionsHandlerErrors, UnpauseScheduleData, UnpauseScheduleResponses, UnpauseScheduleErrors, ListSessionsData, ListSessionsResponses, ListSessionsErrors, GetSessionHistoryData, GetSessionHistoryResponses, GetSessionHistoryErrors, StatusData, StatusResponses } from './types.gen'; import { client as _heyApiClient } from './client.gen'; export type Options = ClientOptions & { @@ -184,6 +184,13 @@ export const providers = (options?: Option }); }; +export const getProviderModels = (options: Options) => { + return (options.client ?? _heyApiClient).get({ + url: '/config/providers/{name}/models', + ...options + }); +}; + export const readConfig = (options: Options) => { return (options.client ?? _heyApiClient).post({ url: '/config/read', @@ -278,6 +285,17 @@ export const decodeRecipe = (options: Opti }); }; +export const deleteRecipe = (options: Options) => { + return (options.client ?? _heyApiClient).post({ + url: '/recipes/delete', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + export const encodeRecipe = (options: Options) => { return (options.client ?? _heyApiClient).post({ url: '/recipes/encode', @@ -289,6 +307,13 @@ export const encodeRecipe = (options: Opti }); }; +export const listRecipes = (options?: Options) => { + return (options?.client ?? _heyApiClient).get({ + url: '/recipes/list', + ...options + }); +}; + export const scanRecipe = (options: Options) => { return (options.client ?? _heyApiClient).post({ url: '/recipes/scan', diff --git a/ui/desktop/src/goosed.ts b/ui/desktop/src/goosed.ts index 3830bace8632..d9c73c755dae 100644 --- a/ui/desktop/src/goosed.ts +++ b/ui/desktop/src/goosed.ts @@ -8,7 +8,7 @@ import log from './utils/logger'; import { App } from 'electron'; import { Buffer } from 'node:buffer'; -import {status} from "./api"; +import { status } from './api'; import { client } from './api/client.gen'; // Find an available port to start goosed on @@ -28,7 +28,7 @@ export const findAvailablePort = (): Promise => { // Goose process manager. Take in the app, port, and directory to start goosed in. // Check if goosed server is ready by polling the status endpoint -const checkServerStatus = async (port: number, secret: string): Promise => { +const checkServerStatus = async (port: number): Promise => { const isTemporalEnabled = process.env.GOOSE_SCHEDULER_TYPE === 'temporal'; const maxAttempts = isTemporalEnabled ? 200 : 80; const interval = 100; @@ -42,7 +42,7 @@ const checkServerStatus = async (port: number, secret: string): Promise for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { - await status({throwOnError: true}); + await status({ throwOnError: true }); } catch { if (attempt === maxAttempts) { log.error(`Server failed to respond after ${maxAttempts} attempts`); @@ -59,7 +59,7 @@ const connectToExternalBackend = async ( ): Promise<[number, string, ChildProcess]> => { log.info(`Using external goosed backend on port ${port}`); - const isReady = await checkServerStatus(port, 'test'); + const isReady = await checkServerStatus(port); if (!isReady) { throw new Error(`External goosed server not accessible on port ${port}`); } @@ -261,7 +261,7 @@ export const startGoosed = async ( }); // Wait for the server to be ready - const isReady = await checkServerStatus(port, serverSecret); + const isReady = await checkServerStatus(port); log.info(`Goosed isReady ${isReady}`); const try_kill_goose = () => { From bc65083c324ffea750513fc4c0af26ae8c6034fa Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Mon, 8 Sep 2025 11:40:04 -0400 Subject: [PATCH 5/8] Don't need these --- crates/goose-server/src/routes/audio.rs | 2 -- ui/desktop/src/goosed.ts | 2 -- 2 files changed, 4 deletions(-) diff --git a/crates/goose-server/src/routes/audio.rs b/crates/goose-server/src/routes/audio.rs index 642ff88eee63..7e1ea768ef40 100644 --- a/crates/goose-server/src/routes/audio.rs +++ b/crates/goose-server/src/routes/audio.rs @@ -209,8 +209,6 @@ async fn send_openai_request( /// - 502: Bad Gateway (OpenAI API error) /// - 503: Service Unavailable (network error) async fn transcribe_handler( - _state: State>, - _headers: HeaderMap, Json(request): Json, ) -> Result, StatusCode> { let (audio_bytes, file_extension) = validate_audio_input(&request.audio, &request.mime_type)?; diff --git a/ui/desktop/src/goosed.ts b/ui/desktop/src/goosed.ts index d9c73c755dae..783e525e0f37 100644 --- a/ui/desktop/src/goosed.ts +++ b/ui/desktop/src/goosed.ts @@ -9,7 +9,6 @@ import { App } from 'electron'; import { Buffer } from 'node:buffer'; import { status } from './api'; -import { client } from './api/client.gen'; // Find an available port to start goosed on export const findAvailablePort = (): Promise => { @@ -38,7 +37,6 @@ const checkServerStatus = async (port: number): Promise => { const statusUrl = `http://127.0.0.1:${port}/status`; log.info(`Checking server status at ${statusUrl}`); - log.info(`Client: ${JSON.stringify(client.getConfig(), undefined, 2)}`); for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { From 89d6ae2baa98a0f5b5874c15c4b4e46e048f9700 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Mon, 8 Sep 2025 11:46:45 -0400 Subject: [PATCH 6/8] Unused imports --- crates/goose-server/src/routes/audio.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/goose-server/src/routes/audio.rs b/crates/goose-server/src/routes/audio.rs index 7e1ea768ef40..473c37b5800e 100644 --- a/crates/goose-server/src/routes/audio.rs +++ b/crates/goose-server/src/routes/audio.rs @@ -4,13 +4,11 @@ /// The OpenAI API key must be configured in the backend for this to work. use crate::state::AppState; use axum::{ - extract::State, http::StatusCode, routing::{get, post}, Json, Router, }; use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; -use http::HeaderMap; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::sync::Arc; From 2de51d8d2e68b1d994281ddf805dfed517c41515 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Mon, 8 Sep 2025 12:57:25 -0400 Subject: [PATCH 7/8] Simplify init --- .../contexts/ClientInitializationContext.tsx | 84 ------------------- ui/desktop/src/goosed.ts | 28 ++++--- ui/desktop/src/renderer.tsx | 33 ++++---- 3 files changed, 33 insertions(+), 112 deletions(-) delete mode 100644 ui/desktop/src/contexts/ClientInitializationContext.tsx diff --git a/ui/desktop/src/contexts/ClientInitializationContext.tsx b/ui/desktop/src/contexts/ClientInitializationContext.tsx deleted file mode 100644 index 02bbce5a41a3..000000000000 --- a/ui/desktop/src/contexts/ClientInitializationContext.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react'; -import { client } from '../api/client.gen'; - -interface ClientInitializationContextType { - isInitialized: boolean; - initializationError: Error | null; -} - -// Track if client has been initialized to avoid duplicate initialization -let clientInitialized = false; - -async function ensureClientInitialized() { - if (clientInitialized) return; - client.setConfig({ - baseUrl: window.appConfig.get('GOOSE_API_HOST') + ':' + window.appConfig.get('GOOSE_PORT'), - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': await window.electron.getSecretKey(), - }, - }); - clientInitialized = true; -} - -const ClientInitializationContext = createContext( - undefined -); - -interface ClientInitializationProviderProps { - children: ReactNode; -} - -export const ClientInitializationProvider: React.FC = ({ - children, -}) => { - const [isInitialized, setIsInitialized] = useState(false); - const [initializationError, setInitializationError] = useState(null); - - useEffect(() => { - const initializeClient = async () => { - try { - await ensureClientInitialized(); - setIsInitialized(true); - } catch (error) { - console.error('Failed to initialize API client:', error); - setInitializationError(error instanceof Error ? error : new Error('Unknown error')); - } - }; - - initializeClient(); - }, []); - - return ( - - {children} - - ); -}; - -export const useClientInitialization = () => { - const context = useContext(ClientInitializationContext); - if (context === undefined) { - throw new Error('useClientInitialization must be used within a ClientInitializationProvider'); - } - return context; -}; - -// Helper component to ensure initialization before rendering children -export const RequireClientInitialization: React.FC<{ children: ReactNode }> = ({ children }) => { - const { isInitialized, initializationError } = useClientInitialization(); - - if (initializationError) { - throw initializationError; - } - - if (!isInitialized) { - return ( -
-
-
- ); - } - - return <>{children}; -}; diff --git a/ui/desktop/src/goosed.ts b/ui/desktop/src/goosed.ts index 783e525e0f37..16c8398f3f52 100644 --- a/ui/desktop/src/goosed.ts +++ b/ui/desktop/src/goosed.ts @@ -9,6 +9,7 @@ import { App } from 'electron'; import { Buffer } from 'node:buffer'; import { status } from './api'; +import { client } from './api/client.gen'; // Find an available port to start goosed on export const findAvailablePort = (): Promise => { @@ -27,23 +28,16 @@ export const findAvailablePort = (): Promise => { // Goose process manager. Take in the app, port, and directory to start goosed in. // Check if goosed server is ready by polling the status endpoint -const checkServerStatus = async (port: number): Promise => { - const isTemporalEnabled = process.env.GOOSE_SCHEDULER_TYPE === 'temporal'; - const maxAttempts = isTemporalEnabled ? 200 : 80; +const checkServerStatus = async (): Promise => { const interval = 100; - log.info( - `Using ${maxAttempts} max attempts (temporal scheduling: ${isTemporalEnabled ? 'enabled' : 'disabled'})` - ); - - const statusUrl = `http://127.0.0.1:${port}/status`; - log.info(`Checking server status at ${statusUrl}`); - + const maxAttempts = 200; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { await status({ throwOnError: true }); + return true; } catch { if (attempt === maxAttempts) { - log.error(`Server failed to respond after ${maxAttempts} attempts`); + log.error(`Server failed to respond after ${(interval * maxAttempts) / 1000} seconds`); } } await new Promise((resolve) => setTimeout(resolve, interval)); @@ -57,7 +51,7 @@ const connectToExternalBackend = async ( ): Promise<[number, string, ChildProcess]> => { log.info(`Using external goosed backend on port ${port}`); - const isReady = await checkServerStatus(port); + const isReady = await checkServerStatus(); if (!isReady) { throw new Error(`External goosed server not accessible on port ${port}`); } @@ -258,8 +252,16 @@ export const startGoosed = async ( throw err; // Propagate the error }); + client.setConfig({ + baseUrl: `http://127.0.0.1:${port}`, + headers: { + 'Content-Type': 'application/json', + 'X-Secret-Key': serverSecret, + }, + }); + // Wait for the server to be ready - const isReady = await checkServerStatus(port); + const isReady = await checkServerStatus(); log.info(`Goosed isReady ${isReady}`); const try_kill_goose = () => { diff --git a/ui/desktop/src/renderer.tsx b/ui/desktop/src/renderer.tsx index 9f3fd6052f42..75047dc9c73d 100644 --- a/ui/desktop/src/renderer.tsx +++ b/ui/desktop/src/renderer.tsx @@ -1,27 +1,30 @@ import React, { Suspense, lazy } from 'react'; import ReactDOM from 'react-dom/client'; import { ConfigProvider } from './components/ConfigContext'; -import { - ClientInitializationProvider, - RequireClientInitialization, -} from './contexts/ClientInitializationContext'; import { ErrorBoundary } from './components/ErrorBoundary'; import SuspenseLoader from './suspense-loader'; +import { client } from './api/client.gen'; const App = lazy(() => import('./App')); -ReactDOM.createRoot(document.getElementById('root')!).render( - +(async () => { + client.setConfig({ + baseUrl: window.appConfig.get('GOOSE_API_HOST') + ':' + window.appConfig.get('GOOSE_PORT'), + headers: { + 'Content-Type': 'application/json', + 'X-Secret-Key': await window.electron.getSecretKey(), + }, + }); + + ReactDOM.createRoot(document.getElementById('root')!).render( - - - - - - - + + + + + - -); + ); +})(); From 946acf2b993f42be538bbb73da8287526c63eb5a Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Mon, 8 Sep 2025 16:13:38 -0400 Subject: [PATCH 8/8] A fix for tool routing setting --- .../tool_selection_strategy/ToolSelectionStrategySection.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui/desktop/src/components/settings/tool_selection_strategy/ToolSelectionStrategySection.tsx b/ui/desktop/src/components/settings/tool_selection_strategy/ToolSelectionStrategySection.tsx index 1aeceb5a6531..3b172c455e7c 100644 --- a/ui/desktop/src/components/settings/tool_selection_strategy/ToolSelectionStrategySection.tsx +++ b/ui/desktop/src/components/settings/tool_selection_strategy/ToolSelectionStrategySection.tsx @@ -53,6 +53,9 @@ export const ToolSelectionStrategySection = () => { 'Content-Type': 'application/json', 'X-Secret-Key': await window.electron.getSecretKey(), }, + body: JSON.stringify({ + session_id: '', // TODO(jack) add the session id, or remove from this request payload + }), }); if (!response.ok) {