feat: update config endpoints for use with providers (#1563)

This commit is contained in:
Lily Delalande
2025-03-10 09:51:54 -07:00
committed by GitHub
parent 3b36591cb5
commit 5df2875c1c
43 changed files with 945 additions and 428 deletions
+11 -2
View File
@@ -1,5 +1,8 @@
use utoipa::OpenApi;
use goose::providers::base::ConfigKey;
use goose::providers::base::ProviderMetadata;
#[allow(dead_code)] // Used by utoipa for OpenAPI generation
#[derive(OpenApi)]
#[openapi(
@@ -10,13 +13,19 @@ use utoipa::OpenApi;
super::routes::config_management::add_extension,
super::routes::config_management::remove_extension,
super::routes::config_management::update_extension,
super::routes::config_management::read_all_config
super::routes::config_management::read_all_config,
super::routes::config_management::providers
),
components(schemas(
super::routes::config_management::UpsertConfigQuery,
super::routes::config_management::ConfigKeyQuery,
super::routes::config_management::ExtensionQuery,
super::routes::config_management::ConfigResponse
super::routes::config_management::ConfigResponse,
super::routes::config_management::ProvidersResponse,
super::routes::config_management::ProvidersResponse,
super::routes::config_management::ProviderDetails,
ProviderMetadata,
ConfigKey
))
)]
pub struct ApiDoc;
+1 -1
View File
@@ -121,7 +121,7 @@ async fn create_agent(
let config = Config::global();
let model = payload.model.unwrap_or_else(|| {
config
.get("GOOSE_MODEL")
.get_param("GOOSE_MODEL")
.expect("Did not find a model on payload or in env")
});
let model_config = ModelConfig::new(model);
@@ -5,25 +5,42 @@ use axum::{
Json, Router,
};
use goose::config::Config;
use http::StatusCode;
use goose::providers::base::ProviderMetadata;
use goose::providers::providers as get_providers;
use http::{HeaderMap, StatusCode};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::{collections::HashMap, sync::Arc};
use tokio::sync::Mutex;
use std::collections::HashMap;
use std::env;
use utoipa::ToSchema;
use crate::state::AppState;
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)
}
}
#[derive(Deserialize, ToSchema)]
pub struct UpsertConfigQuery {
pub key: String,
pub value: Value,
pub is_secret: Option<bool>,
pub is_secret: bool,
}
#[derive(Deserialize, ToSchema)]
pub struct ConfigKeyQuery {
pub key: String,
pub is_secret: bool,
}
#[derive(Deserialize, ToSchema)]
@@ -37,6 +54,22 @@ pub struct ConfigResponse {
pub config: HashMap<String, Value>,
}
// Define a new structure to encapsulate the provider details along with configuration status
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct ProviderDetails {
/// Unique identifier and name of the provider
pub name: String,
/// Metadata about the provider
pub metadata: ProviderMetadata,
/// Indicates whether the provider is fully configured
pub is_configured: bool,
}
#[derive(Serialize, ToSchema)]
pub struct ProvidersResponse {
pub providers: Vec<ProviderDetails>,
}
#[utoipa::path(
post,
path = "/config/upsert",
@@ -47,16 +80,15 @@ pub struct ConfigResponse {
)
)]
pub async fn upsert_config(
State(_state): State<Arc<Mutex<HashMap<String, Value>>>>,
State(state): State<AppState>,
headers: HeaderMap,
Json(query): Json<UpsertConfigQuery>,
) -> Result<Json<Value>, StatusCode> {
let config = Config::global();
// Use the helper function to verify the secret key
verify_secret_key(&headers, &state)?;
let result = if query.is_secret.unwrap_or(false) {
config.set_secret(&query.key, query.value)
} else {
config.set(&query.key, query.value)
};
let config = Config::global();
let result = config.set(&query.key, query.value, query.is_secret);
match result {
Ok(_) => Ok(Json(Value::String(format!("Upserted key {}", query.key)))),
@@ -75,9 +107,13 @@ pub async fn upsert_config(
)
)]
pub async fn remove_config(
State(_state): State<Arc<Mutex<HashMap<String, Value>>>>,
State(state): State<AppState>,
headers: HeaderMap,
Json(query): Json<ConfigKeyQuery>,
) -> Result<Json<String>, StatusCode> {
// Use the helper function to verify the secret key
verify_secret_key(&headers, &state)?;
let config = Config::global();
match config.delete(&query.key) {
@@ -96,13 +132,25 @@ pub async fn remove_config(
)
)]
pub async fn read_config(
State(_state): State<Arc<Mutex<HashMap<String, Value>>>>,
State(state): State<AppState>,
headers: HeaderMap,
Json(query): Json<ConfigKeyQuery>,
) -> Result<Json<Value>, StatusCode> {
verify_secret_key(&headers, &state)?;
let config = Config::global();
match config.get::<Value>(&query.key) {
Ok(value) => Ok(Json(value)),
match config.get(&query.key, query.is_secret) {
// Always get the actual value
Ok(value) => {
if query.is_secret {
// If it's marked as secret, return a boolean indicating presence
Ok(Json(Value::Bool(true)))
} else {
// Return the actual value if not secret
Ok(Json(value))
}
}
Err(_) => Err(StatusCode::NOT_FOUND),
}
}
@@ -118,20 +166,25 @@ pub async fn read_config(
)
)]
pub async fn add_extension(
State(_state): State<Arc<Mutex<HashMap<String, Value>>>>,
State(state): State<AppState>,
headers: HeaderMap,
Json(extension): Json<ExtensionQuery>,
) -> Result<Json<String>, StatusCode> {
// Use the helper function to verify the secret key
verify_secret_key(&headers, &state)?;
let config = Config::global();
// Get current extensions or initialize empty map
let mut extensions: HashMap<String, Value> =
config.get("extensions").unwrap_or_else(|_| HashMap::new());
let mut extensions: HashMap<String, Value> = config
.get_param("extensions")
.unwrap_or_else(|_| HashMap::new());
// Add new extension
extensions.insert(extension.name.clone(), extension.config);
// Save updated extensions
match config.set(
match config.set_param(
"extensions",
Value::Object(extensions.into_iter().collect()),
) {
@@ -151,13 +204,17 @@ pub async fn add_extension(
)
)]
pub async fn remove_extension(
State(_state): State<Arc<Mutex<HashMap<String, Value>>>>,
State(state): State<AppState>,
headers: HeaderMap,
Json(query): Json<ConfigKeyQuery>,
) -> Result<Json<String>, StatusCode> {
// Use the helper function to verify the secret key
verify_secret_key(&headers, &state)?;
let config = Config::global();
// Get current extensions
let mut extensions: HashMap<String, Value> = match config.get("extensions") {
let mut extensions: HashMap<String, Value> = match config.get_param("extensions") {
Ok(exts) => exts,
Err(_) => return Err(StatusCode::NOT_FOUND),
};
@@ -165,7 +222,7 @@ pub async fn remove_extension(
// Remove extension if it exists
if extensions.remove(&query.key).is_some() {
// Save updated extensions
match config.set(
match config.set_param(
"extensions",
Value::Object(extensions.into_iter().collect()),
) {
@@ -185,8 +242,12 @@ pub async fn remove_extension(
)
)]
pub async fn read_all_config(
State(_state): State<Arc<Mutex<HashMap<String, Value>>>>,
State(state): State<AppState>,
headers: HeaderMap,
) -> Result<Json<ConfigResponse>, StatusCode> {
// Use the helper function to verify the secret key
verify_secret_key(&headers, &state)?;
let config = Config::global();
// Load values from config file
@@ -206,13 +267,17 @@ pub async fn read_all_config(
)
)]
pub async fn update_extension(
State(_state): State<Arc<Mutex<HashMap<String, Value>>>>,
State(state): State<AppState>,
headers: HeaderMap,
Json(extension): Json<ExtensionQuery>,
) -> Result<Json<String>, StatusCode> {
// Use the helper function to verify the secret key
verify_secret_key(&headers, &state)?;
let config = Config::global();
// Get current extensions
let mut extensions: HashMap<String, Value> = match config.get("extensions") {
let mut extensions: HashMap<String, Value> = match config.get_param("extensions") {
Ok(exts) => exts,
Err(_) => return Err(StatusCode::NOT_FOUND),
};
@@ -226,7 +291,7 @@ pub async fn update_extension(
extensions.insert(extension.name.clone(), extension.config);
// Save updated extensions
match config.set(
match config.set_param(
"extensions",
Value::Object(extensions.into_iter().collect()),
) {
@@ -235,6 +300,66 @@ pub async fn update_extension(
}
}
// Modified providers function using the new response type
#[utoipa::path(
get,
path = "/config/providers",
responses(
(status = 200, description = "All configuration values retrieved successfully", body = [ProviderDetails])
)
)]
pub async fn providers(
State(state): State<AppState>,
headers: HeaderMap,
) -> Result<Json<Vec<ProviderDetails>>, StatusCode> {
verify_secret_key(&headers, &state)?;
// Fetch the list of providers, which are likely stored in the AppState or can be retrieved via a function call
let providers_metadata = get_providers();
// Construct the response by checking configuration status for each provider
let providers_response: Vec<ProviderDetails> = providers_metadata
.into_iter()
.map(|metadata| {
// Check if the provider is configured (this will depend on how you track configuration status)
let is_configured = check_provider_configured(&metadata);
ProviderDetails {
name: metadata.name.clone(),
metadata,
is_configured,
}
})
.collect();
Ok(Json(providers_response))
}
fn check_provider_configured(metadata: &ProviderMetadata) -> bool {
let config = Config::global();
// Check all required keys for the provider
for key in &metadata.config_keys {
if key.required {
let key_name = &key.name;
// First, check if the key is set in the environment
let is_set_in_env = env::var(key_name).is_ok();
// If not set in environment, check the config file based on whether it's a secret or not
let is_set_in_config = config.get(key_name, key.secret).is_ok();
// If the key is neither in the environment nor in the config, the provider is not configured
if !is_set_in_env && !is_set_in_config {
return false;
}
}
}
// If all required keys are accounted for, the provider is considered configured
true
}
pub fn routes(state: AppState) -> Router {
Router::new()
.route("/config", get(read_all_config))
@@ -244,5 +369,6 @@ pub fn routes(state: AppState) -> Router {
.route("/config/extension", post(add_extension))
.route("/config/extension", put(update_extension))
.route("/config/extension", delete(remove_extension))
.with_state(state.config)
.route("/config/providers", get(providers))
.with_state(state)
}
+3 -3
View File
@@ -43,7 +43,7 @@ async fn store_config(
let result = if request.is_secret {
config.set_secret(&request.key, Value::String(request.value))
} else {
config.set(&request.key, Value::String(request.value))
config.set_param(&request.key, Value::String(request.value))
};
match result {
Ok(_) => Ok(Json(ConfigResponse { error: false })),
@@ -87,7 +87,7 @@ static PROVIDER_ENV_REQUIREMENTS: Lazy<HashMap<String, ProviderConfig>> = Lazy::
fn check_key_status(config: &Config, key: &str) -> (bool, Option<String>) {
if let Ok(_value) = std::env::var(key) {
(true, Some("env".to_string()))
} else if config.get::<String>(key).is_ok() {
} else if config.get_param::<String>(key).is_ok() {
(true, Some("yaml".to_string()))
} else if config.get_secret::<String>(key).is_ok() {
(true, Some("keyring".to_string()))
@@ -171,7 +171,7 @@ pub async fn get_config(
// Fetch the configuration value. Right now we don't allow get a secret.
let config = Config::global();
let value = if let Ok(config_value) = config.get::<String>(&query.key) {
let value = if let Ok(config_value) = config.get_param::<String>(&query.key) {
Some(config_value)
} else if let Ok(env_value) = std::env::var(&query.key) {
Some(env_value)
+92
View File
@@ -0,0 +1,92 @@
use serde::{Deserialize, Serialize};
use std::error::Error;
use goose::config::Config;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum KeyLocation {
Environment,
ConfigFile,
Keychain,
NotFound
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyInfo {
pub name: String,
pub is_set: bool,
pub location: KeyLocation,
pub is_secret: bool,
pub value: Option<String>, // Only populated for non-secret keys that are set
}
/// Inspects a configuration key to determine if it's set, its location, and value (for non-secret keys)
pub fn inspect_key(
key_name: &str,
is_secret: bool,
) -> Result<KeyInfo, Box<dyn Error>> {
let config = Config::global();
// Check environment variable first
let env_value = std::env::var(key_name).ok();
if let Some(value) = env_value {
return Ok(KeyInfo {
name: key_name.to_string(),
is_set: true,
location: KeyLocation::Environment,
is_secret,
// Only include value for non-secret keys
value: if !is_secret { Some(value) } else { None },
});
}
// Check config store
let config_result = if is_secret {
config.get_secret(key_name).map(|v| (v, true))
} else {
config.get(key_name).map(|v| (v, false))
};
match config_result {
Ok((value, is_secret_actual)) => {
// Determine location based on whether it's a secret value
let location = if is_secret_actual {
KeyLocation::Keychain
} else {
KeyLocation::ConfigFile
};
Ok(KeyInfo {
name: key_name.to_string(),
is_set: true,
location,
is_secret: is_secret_actual,
// Only include value for non-secret keys
value: if !is_secret_actual { Some(value) } else { None },
})
},
Err(_) => {
Ok(KeyInfo {
name: key_name.to_string(),
is_set: false,
location: KeyLocation::NotFound,
is_secret,
value: None,
})
}
}
}
/// Inspects multiple keys at once
pub fn inspect_keys(
keys: &[(String, bool)], // (name, is_secret) pairs
) -> Result<Vec<KeyInfo>, Box<dyn Error>> {
let mut results = Vec::new();
for (key_name, is_secret) in keys {
let info = inspect_key(key_name, *is_secret)?;
results.push(info);
}
Ok(results)
}