Use middleware to verify secret key (#4338)
This commit is contained in:
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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,3 +1,4 @@
|
||||
pub mod auth;
|
||||
pub mod openapi;
|
||||
pub mod routes;
|
||||
pub mod state;
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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,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![]);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,4 +1,3 @@
|
||||
// Export route modules
|
||||
pub mod agent;
|
||||
pub mod audio;
|
||||
pub mod config_management;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -1504,6 +1504,26 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/status": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"super::routes::health"
|
||||
],
|
||||
"operationId": "status",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "ok",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
@@ -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 & {});
|
||||
};
|
||||
+3
@@ -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
@@ -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 = () => {
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
);
|
||||
})();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user