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())