Use middleware to verify secret key (#4338)

This commit is contained in:
Jack Amadeo
2025-09-09 09:54:33 -04:00
committed by GitHub
parent 0969b80877
commit 9f53d39b88
27 changed files with 166 additions and 460 deletions
+22
View File
@@ -0,0 +1,22 @@
use axum::{
extract::{Request, State},
http::StatusCode,
middleware::Next,
response::Response,
};
pub async fn check_token(
State(state): State<String>,
request: Request,
next: Next,
) -> Result<Response, StatusCode> {
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),
}
}
+9 -2
View File
@@ -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::check_token;
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,12 @@ 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(),
check_token,
))
.layer(cors);
let listener = tokio::net::TcpListener::bind(settings.socket_addr()).await?;
info!("listening on {}", listener.local_addr()?);
+1
View File
@@ -1,3 +1,4 @@
pub mod auth;
pub mod openapi;
pub mod routes;
pub mod state;
+1
View File
@@ -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,
+1 -35
View File
@@ -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<Arc<AppState>>,
headers: HeaderMap,
Json(payload): Json<StartAgentRequest>,
) -> Result<Json<StartAgentResponse>, 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<Arc<AppState>>,
headers: HeaderMap,
Json(payload): Json<ResumeAgentRequest>,
) -> Result<Json<StartAgentResponse>, 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<Arc<AppState>>,
headers: HeaderMap,
Json(payload): Json<AddSubRecipesRequest>,
) -> Result<Json<AddSubRecipesResponse>, 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<Arc<AppState>>,
headers: HeaderMap,
Json(payload): Json<ExtendPromptRequest>,
) -> Result<Json<ExtendPromptResponse>, 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<Arc<AppState>>,
headers: HeaderMap,
Query(query): Query<GetToolsQuery>,
) -> Result<Json<Vec<ToolInfo>>, 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<Arc<AppState>>,
headers: HeaderMap,
Json(payload): Json<UpdateProviderRequest>,
) -> Result<StatusCode, impl IntoResponse> {
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<Arc<AppState>>,
headers: HeaderMap,
Json(_payload): Json<UpdateRouterToolSelectorRequest>,
) -> Result<Json<String>, Json<ErrorResponse>> {
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<Arc<AppState>>,
headers: HeaderMap,
Json(payload): Json<SessionConfigRequest>,
) -> Result<Json<String>, Json<ErrorResponse>> {
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;
+6 -33
View File
@@ -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,
};
@@ -209,12 +207,8 @@ async fn send_openai_request(
/// - 502: Bad Gateway (OpenAI API error)
/// - 503: Service Unavailable (network error)
async fn transcribe_handler(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Json(request): Json<TranscribeRequest>,
) -> Result<Json<TranscribeResponse>, 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()?;
@@ -237,12 +231,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<Arc<AppState>>,
headers: HeaderMap,
Json(request): Json<TranscribeElevenLabsRequest>,
) -> Result<Json<TranscribeResponse>, 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 +359,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<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Json<serde_json::Value>, StatusCode> {
verify_secret_key(&headers, &state)?;
async fn check_dictation_config() -> Result<Json<serde_json::Value>, StatusCode> {
let config = goose::config::Config::global();
// Check if ElevenLabs API key is configured
@@ -410,10 +395,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()));
let app = routes(state);
// Test without auth header
@@ -436,10 +418,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()));
let app = routes(state);
// Create a large base64 string (simulating > 25MB audio)
@@ -465,10 +444,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()));
let app = routes(state);
let request = Request::builder()
@@ -494,10 +470,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()));
let app = routes(state);
let request = Request::builder()
@@ -1,8 +1,7 @@
use super::utils::verify_secret_key;
use crate::routes::utils::check_provider_configured;
use crate::state::AppState;
use axum::{
extract::{Path, State},
extract::Path,
routing::{delete, get, post},
Json, Router,
};
@@ -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<Arc<AppState>>,
headers: HeaderMap,
Json(query): Json<UpsertConfigQuery>,
) -> Result<Json<Value>, 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<Arc<AppState>>,
headers: HeaderMap,
Json(query): Json<ConfigKeyQuery>,
) -> Result<Json<String>, StatusCode> {
verify_secret_key(&headers, &state)?;
pub async fn remove_config(Json(query): Json<ConfigKeyQuery>) -> Result<Json<String>, 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<Arc<AppState>>,
headers: HeaderMap,
Json(query): Json<ConfigKeyQuery>,
) -> Result<Json<Value>, StatusCode> {
verify_secret_key(&headers, &state)?;
pub async fn read_config(Json(query): Json<ConfigKeyQuery>) -> Result<Json<Value>, 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<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Json<ExtensionResponse>, StatusCode> {
verify_secret_key(&headers, &state)?;
pub async fn get_extensions() -> Result<Json<ExtensionResponse>, 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<Arc<AppState>>,
headers: HeaderMap,
Json(extension_query): Json<ExtensionQuery>,
) -> Result<Json<String>, 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<Arc<AppState>>,
headers: HeaderMap,
axum::extract::Path(name): axum::extract::Path<String>,
) -> Result<Json<String>, 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<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Json<ConfigResponse>, StatusCode> {
verify_secret_key(&headers, &state)?;
pub async fn read_all_config() -> Result<Json<ConfigResponse>, 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<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Json<Vec<ProviderDetails>>, StatusCode> {
verify_secret_key(&headers, &state)?;
pub async fn providers() -> Result<Json<Vec<ProviderDetails>>, StatusCode> {
let mut providers_metadata = get_providers();
let custom_providers_dir = goose::config::custom_providers::custom_providers_dir();
@@ -403,12 +363,8 @@ pub async fn providers(
)
)]
pub async fn get_provider_models(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Path(name): Path<String>,
) -> Result<Json<Vec<String>>, 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);
@@ -480,12 +436,8 @@ pub struct PricingQuery {
)
)]
pub async fn get_pricing(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Json(query): Json<PricingQuery>,
) -> Result<Json<PricingResponse>, 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 +530,7 @@ pub async fn get_pricing(
(status = 500, description = "Internal server error")
)
)]
pub async fn init_config(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Json<String>, StatusCode> {
verify_secret_key(&headers, &state)?;
pub async fn init_config() -> Result<Json<String>, StatusCode> {
let config = Config::global();
if config.exists() {
@@ -612,12 +559,8 @@ pub async fn init_config(
)
)]
pub async fn upsert_permissions(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Json(query): Json<UpsertPermissionsQuery>,
) -> Result<Json<String>, StatusCode> {
verify_secret_key(&headers, &state)?;
let mut permission_manager = goose::config::PermissionManager::default();
for tool_permission in &query.tool_permissions {
@@ -638,12 +581,7 @@ pub async fn upsert_permissions(
(status = 500, description = "Internal server error")
)
)]
pub async fn backup_config(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Json<String>, StatusCode> {
verify_secret_key(&headers, &state)?;
pub async fn backup_config() -> Result<Json<String>, StatusCode> {
let config_dir = choose_app_strategy(APP_STRATEGY.clone())
.expect("goose requires a home dir")
.config_dir();
@@ -676,12 +614,7 @@ pub async fn backup_config(
(status = 500, description = "Internal server error")
)
)]
pub async fn recover_config(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Json<String>, StatusCode> {
verify_secret_key(&headers, &state)?;
pub async fn recover_config() -> Result<Json<String>, StatusCode> {
let config = Config::global();
// Force a reload which will trigger recovery if needed
@@ -713,12 +646,7 @@ pub async fn recover_config(
(status = 422, description = "Config file is corrupted")
)
)]
pub async fn validate_config(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Json<String>, StatusCode> {
verify_secret_key(&headers, &state)?;
pub async fn validate_config() -> Result<Json<String>, StatusCode> {
let config_dir = choose_app_strategy(APP_STRATEGY.clone())
.expect("goose requires a home dir")
.config_dir();
@@ -751,12 +679,7 @@ pub async fn validate_config(
(status = 200, description = "Current model retrieved successfully", body = String),
)
)]
pub async fn get_current_model(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Json<Value>, StatusCode> {
verify_secret_key(&headers, &state)?;
pub async fn get_current_model() -> Result<Json<Value>, StatusCode> {
let current_model = goose::providers::base::get_current_model();
Ok(Json(serde_json::json!({
@@ -775,12 +698,8 @@ pub async fn get_current_model(
)
)]
pub async fn create_custom_provider(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Json(request): Json<CreateCustomProviderRequest>,
) -> Result<Json<String>, StatusCode> {
verify_secret_key(&headers, &state)?;
let config = goose::config::custom_providers::CustomProviderConfig::create_and_save(
&request.provider_type,
request.display_name,
@@ -808,12 +727,8 @@ pub async fn create_custom_provider(
)
)]
pub async fn remove_custom_provider(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
axum::extract::Path(id): axum::extract::Path<String>,
) -> Result<Json<String>, StatusCode> {
verify_secret_key(&headers, &state)?;
goose::config::custom_providers::CustomProviderConfig::remove(&id)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
@@ -852,38 +767,19 @@ pub fn routes(state: Arc<AppState>) -> Router {
#[cfg(test)]
mod tests {
use super::*;
use http::HeaderMap;
async fn create_test_state() -> Arc<AppState> {
let test_state = AppState::new(
Arc::new(goose::agents::Agent::default()),
"test".to_string(),
);
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
}
use super::*;
#[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());
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());
@@ -900,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);
@@ -919,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!(
+1 -10
View File
@@ -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<Arc<AppState>>,
headers: HeaderMap,
Json(request): Json<ContextManageRequest>,
) -> Result<Json<ContextManageResponse>, StatusCode> {
verify_secret_key(&headers, &state)?;
let agent = state.get_agent().await;
let mut processed_messages = Conversation::new_unvalidated(vec![]);
+1 -8
View File
@@ -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<Arc<AppState>>,
headers: HeaderMap,
raw: axum::extract::Json<serde_json::Value>,
) -> Result<Json<ExtensionResponse>, 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<Arc<AppState>>,
headers: HeaderMap,
Json(name): Json<String>,
) -> Result<Json<ExtensionResponse>, StatusCode> {
verify_secret_key(&headers, &state)?;
let agent = state.get_agent().await;
match agent.remove_extension(&name).await {
Ok(_) => Ok(Json(ExtensionResponse {
+8 -11
View File
@@ -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<StatusResponse> {
Json(StatusResponse { status: "ok" })
}
/// Configure health check routes
pub fn routes() -> Router {
Router::new().route("/status", get(status))
}
-1
View File
@@ -1,4 +1,3 @@
// Export route modules
pub mod agent;
pub mod audio;
pub mod config_management;
-9
View File
@@ -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<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Json<ListRecipeResponse>, 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<Arc<AppState>>,
headers: HeaderMap,
Json(request): Json<DeleteRecipeRequest>,
) -> 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,
+2 -12
View File
@@ -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<Arc<AppState>>,
headers: HeaderMap,
Json(request): Json<ChatRequest>,
) -> Result<SseResponse, StatusCode> {
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<Arc<AppState>>,
headers: HeaderMap,
Json(request): Json<PermissionConfirmationRequest>,
) -> Result<Json<Value>, 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<Arc<AppState>>,
headers: HeaderMap,
raw: Json<Value>,
) -> Result<Json<Value>, 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);
+1 -22
View File
@@ -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<Arc<AppState>>,
headers: HeaderMap,
Json(req): Json<CreateScheduleRequest>,
) -> Result<Json<ScheduledJob>, 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<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Json<ListSchedulesResponse>, 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<Arc<AppState>>,
headers: HeaderMap,
Path(id): Path<String>,
) -> Result<StatusCode, StatusCode> {
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<Arc<AppState>>,
headers: HeaderMap,
Path(id): Path<String>,
) -> Result<Json<RunNowResponse>, 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<Arc<AppState>>,
headers: HeaderMap, // Added this line
Path(schedule_id_param): Path<String>, // Renamed to avoid confusion with session_id
Query(query_params): Query<SessionsQuery>,
) -> Result<Json<Vec<SessionDisplayInfo>>, 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<Arc<AppState>>,
headers: HeaderMap,
Path(id): Path<String>,
) -> Result<StatusCode, StatusCode> {
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<Arc<AppState>>,
headers: HeaderMap,
Path(id): Path<String>,
) -> Result<StatusCode, StatusCode> {
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<Arc<AppState>>,
headers: HeaderMap,
Path(id): Path<String>,
Json(req): Json<UpdateScheduleRequest>,
) -> Result<Json<ScheduledJob>, 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<Arc<AppState>>,
headers: HeaderMap,
Path(id): Path<String>,
) -> Result<Json<KillJobResponse>, 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<Arc<AppState>>,
headers: HeaderMap,
Path(id): Path<String>,
) -> Result<Json<InspectJobResponse>, StatusCode> {
verify_secret_key(&headers, &state)?;
let scheduler = state
.scheduler()
.await
+5 -30
View File
@@ -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<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Json<SessionListResponse>, StatusCode> {
verify_secret_key(&headers, &state)?;
async fn list_sessions() -> Result<Json<SessionListResponse>, 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<Arc<AppState>>,
headers: HeaderMap,
Path(session_id): Path<String>,
) -> Result<Json<SessionHistoryResponse>, 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<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Json<SessionInsights>, StatusCode> {
async fn get_session_insights() -> Result<Json<SessionInsights>, 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<Arc<AppState>>,
headers: HeaderMap,
Path(session_id): Path<String>,
Json(request): Json<UpdateSessionMetadataRequest>,
) -> Result<StatusCode, StatusCode> {
verify_secret_key(&headers, &state)?;
// Validate description length
if request.description.len() > MAX_DESCRIPTION_LENGTH {
return Err(StatusCode::BAD_REQUEST);
@@ -328,13 +309,7 @@ async fn update_session_metadata(
tag = "Session Management"
)]
// Delete a session
async fn delete_session(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Path(session_id): Path<String>,
) -> Result<StatusCode, StatusCode> {
verify_secret_key(&headers, &state)?;
async fn delete_session(Path(session_id): Path<String>) -> Result<StatusCode, StatusCode> {
// Get the session path
let session_path = match session::get_path(session::Identifier::Name(session_id.clone())) {
Ok(path) => path,
-16
View File
@@ -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<String>, // Only populated for non-secret keys that are set
}
pub fn verify_secret_key(headers: &HeaderMap, state: &AppState) -> Result<StatusCode, StatusCode> {
// 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<KeyInfo, Box<dyn Error>> {
+1 -3
View File
@@ -12,17 +12,15 @@ type AgentRef = Arc<Agent>;
#[derive(Clone)]
pub struct AppState {
agent: Arc<RwLock<AgentRef>>,
pub secret_key: String,
pub scheduler: Arc<RwLock<Option<Arc<dyn SchedulerTrait>>>>,
pub recipe_file_hash_map: Arc<Mutex<HashMap<String, PathBuf>>>,
pub session_counter: Arc<AtomicUsize>,
}
impl AppState {
pub fn new(agent: AgentRef, secret_key: String) -> Arc<AppState> {
pub fn new(agent: AgentRef) -> Arc<AppState> {
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)),
@@ -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())
+20
View File
@@ -1504,6 +1504,26 @@
}
]
}
},
"/status": {
"get": {
"tags": [
"super::routes::health"
],
"operationId": "status",
"responses": {
"200": {
"description": "ok",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
}
}
}
}
}
}
},
"components": {
+8 -1
View File
@@ -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, 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<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<TData, ThrowOnError> & {
@@ -415,4 +415,11 @@ export const getSessionHistory = <ThrowOnError extends boolean = false>(options:
url: '/sessions/{session_id}',
...options
});
};
export const status = <ThrowOnError extends boolean = false>(options?: Options<StatusData, ThrowOnError>) => {
return (options?.client ?? _heyApiClient).get<StatusResponses, unknown, ThrowOnError>({
url: '/status',
...options
});
};
+16
View File
@@ -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 & {});
};
@@ -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) {
@@ -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<ClientInitializationContextType | undefined>(
undefined
);
interface ClientInitializationProviderProps {
children: ReactNode;
}
export const ClientInitializationProvider: React.FC<ClientInitializationProviderProps> = ({
children,
}) => {
const [isInitialized, setIsInitialized] = useState(false);
const [initializationError, setInitializationError] = useState<Error | null>(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 (
<ClientInitializationContext.Provider value={{ isInitialized, initializationError }}>
{children}
</ClientInitializationContext.Provider>
);
};
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 (
<div className="flex justify-center items-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-textStandard"></div>
</div>
);
}
return <>{children}</>;
};
+19 -25
View File
@@ -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<number> => {
return new Promise((resolve, _reject) => {
@@ -25,33 +28,16 @@ export const findAvailablePort = (): Promise<number> => {
// 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<boolean> => {
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 statusUrl = `http://127.0.0.1:${port}/status`;
log.info(`Checking server status at ${statusUrl}`);
const checkServerStatus = async (): Promise<boolean> => {
const interval = 100;
const maxAttempts = 200;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const response = await fetch(statusUrl);
if (response.ok) {
log.info(`Server is ready after ${attempt} attempts`);
return true;
}
await status({ throwOnError: true });
return true;
} catch {
// Expected error when server isn't ready yet
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));
@@ -65,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}`);
}
@@ -266,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 = () => {
+1 -1
View File
@@ -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,
+18 -18
View File
@@ -1,30 +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 { patchConsoleLogging } from './utils';
import SuspenseLoader from './suspense-loader';
patchConsoleLogging();
import { client } from './api/client.gen';
const App = lazy(() => import('./App'));
ReactDOM.createRoot(document.getElementById('root')!).render(
<ClientInitializationProvider>
(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(
<React.StrictMode>
<Suspense fallback={SuspenseLoader()}>
<RequireClientInitialization>
<ConfigProvider>
<ErrorBoundary>
<App />
</ErrorBoundary>
</ConfigProvider>
</RequireClientInitialization>
<ConfigProvider>
<ErrorBoundary>
<App />
</ErrorBoundary>
</ConfigProvider>
</Suspense>
</React.StrictMode>
</ClientInitializationProvider>
);
);
})();
+1 -6
View File
@@ -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;
}
}