feat: extensions read config (#1637)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
use cliclack::spinner;
|
||||
use console::style;
|
||||
use goose::agents::{extension::Envs, ExtensionConfig};
|
||||
use goose::config::extensions::name_to_key;
|
||||
use goose::config::{Config, ConfigError, ExperimentManager, ExtensionEntry, ExtensionManager};
|
||||
use goose::message::Message;
|
||||
use goose::providers::{create, providers};
|
||||
@@ -387,7 +388,10 @@ pub fn toggle_extensions_dialog() -> Result<(), Box<dyn Error>> {
|
||||
|
||||
// Update enabled status for each extension
|
||||
for name in extension_status.iter().map(|(name, _)| name) {
|
||||
ExtensionManager::set_enabled(name, selected.iter().any(|s| s.as_str() == name))?;
|
||||
ExtensionManager::set_enabled(
|
||||
&name_to_key(name),
|
||||
selected.iter().any(|s| s.as_str() == name),
|
||||
)?;
|
||||
}
|
||||
|
||||
cliclack::outro("Extension settings updated successfully")?;
|
||||
@@ -638,10 +642,17 @@ pub fn remove_extension_dialog() -> Result<(), Box<dyn Error>> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Filter out only disabled extensions
|
||||
let disabled_extensions: Vec<_> = extensions
|
||||
.iter()
|
||||
.filter(|entry| !entry.enabled)
|
||||
.map(|entry| (entry.config.name().to_string(), entry.enabled))
|
||||
.collect();
|
||||
|
||||
let selected = cliclack::multiselect("Select extensions to remove (note: you can only remove disabled extensions - use \"space\" to toggle and \"enter\" to submit)")
|
||||
.required(false)
|
||||
.items(
|
||||
&extension_status
|
||||
&disabled_extensions
|
||||
.iter()
|
||||
.filter(|(_, enabled)| !enabled)
|
||||
.map(|(name, _)| (name, name.as_str(), ""))
|
||||
@@ -650,7 +661,7 @@ pub fn remove_extension_dialog() -> Result<(), Box<dyn Error>> {
|
||||
.interact()?;
|
||||
|
||||
for name in selected {
|
||||
ExtensionManager::remove(name)?;
|
||||
ExtensionManager::remove(&name_to_key(name))?;
|
||||
cliclack::outro(format!("Removed {} extension", style(name).green()))?;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use utoipa::OpenApi;
|
||||
|
||||
use goose::agents::extension::Envs;
|
||||
use goose::agents::ExtensionConfig;
|
||||
use goose::config::ExtensionEntry;
|
||||
use goose::providers::base::ConfigKey;
|
||||
use goose::providers::base::ProviderMetadata;
|
||||
|
||||
@@ -12,6 +15,8 @@ use goose::providers::base::ProviderMetadata;
|
||||
super::routes::config_management::read_config,
|
||||
super::routes::config_management::add_extension,
|
||||
super::routes::config_management::remove_extension,
|
||||
super::routes::config_management::toggle_extension,
|
||||
super::routes::config_management::get_extensions,
|
||||
super::routes::config_management::update_extension,
|
||||
super::routes::config_management::read_all_config,
|
||||
super::routes::config_management::providers
|
||||
@@ -19,13 +24,17 @@ use goose::providers::base::ProviderMetadata;
|
||||
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::ProvidersResponse,
|
||||
super::routes::config_management::ProvidersResponse,
|
||||
super::routes::config_management::ProviderDetails,
|
||||
super::routes::config_management::ExtensionResponse,
|
||||
super::routes::config_management::ExtensionQuery,
|
||||
ProviderMetadata,
|
||||
ConfigKey
|
||||
ExtensionEntry,
|
||||
ExtensionConfig,
|
||||
ConfigKey,
|
||||
Envs,
|
||||
))
|
||||
)]
|
||||
pub struct ApiDoc;
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
use crate::routes::utils::check_provider_configured;
|
||||
use crate::state::AppState;
|
||||
use axum::routing::put;
|
||||
use axum::{
|
||||
extract::State,
|
||||
routing::{delete, get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use goose::agents::ExtensionConfig;
|
||||
use goose::config::extensions::name_to_key;
|
||||
use goose::config::Config;
|
||||
use goose::config::{ExtensionEntry, ExtensionManager};
|
||||
use goose::providers::base::ProviderMetadata;
|
||||
use goose::providers::providers as get_providers;
|
||||
use http::{HeaderMap, StatusCode};
|
||||
@@ -13,9 +18,6 @@ use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::routes::utils::check_provider_configured;
|
||||
use crate::state::AppState;
|
||||
|
||||
fn verify_secret_key(headers: &HeaderMap, state: &AppState) -> Result<StatusCode, StatusCode> {
|
||||
// Verify secret key
|
||||
let secret_key = headers
|
||||
@@ -30,6 +32,18 @@ fn verify_secret_key(headers: &HeaderMap, state: &AppState) -> Result<StatusCode
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ExtensionResponse {
|
||||
pub extensions: Vec<ExtensionEntry>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct ExtensionQuery {
|
||||
pub name: String,
|
||||
pub config: ExtensionConfig,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct UpsertConfigQuery {
|
||||
pub key: String,
|
||||
@@ -43,12 +57,6 @@ pub struct ConfigKeyQuery {
|
||||
pub is_secret: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct ExtensionQuery {
|
||||
pub name: String,
|
||||
pub config: Value,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ConfigResponse {
|
||||
pub config: HashMap<String, Value>,
|
||||
@@ -155,9 +163,29 @@ pub async fn read_config(
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/config/extensions",
|
||||
responses(
|
||||
(status = 200, description = "All extensions retrieved successfully", body = ExtensionResponse),
|
||||
(status = 500, description = "Internal server error")
|
||||
)
|
||||
)]
|
||||
pub async fn get_extensions(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Json<ExtensionResponse>, StatusCode> {
|
||||
verify_secret_key(&headers, &state)?;
|
||||
|
||||
match ExtensionManager::get_all() {
|
||||
Ok(extensions) => Ok(Json(ExtensionResponse { extensions })),
|
||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/config/extension",
|
||||
path = "/config/extensions",
|
||||
request_body = ExtensionQuery,
|
||||
responses(
|
||||
(status = 200, description = "Extension added successfully", body = String),
|
||||
@@ -168,35 +196,23 @@ pub async fn read_config(
|
||||
pub async fn add_extension(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Json(extension): Json<ExtensionQuery>,
|
||||
Json(extension_query): 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_param("extensions")
|
||||
.unwrap_or_else(|_| HashMap::new());
|
||||
|
||||
// Add new extension
|
||||
extensions.insert(extension.name.clone(), extension.config);
|
||||
|
||||
// Save updated extensions
|
||||
match config.set_param(
|
||||
"extensions",
|
||||
Value::Object(extensions.into_iter().collect()),
|
||||
) {
|
||||
Ok(_) => Ok(Json(format!("Added extension {}", extension.name))),
|
||||
// Use ExtensionManager to set the extension
|
||||
match ExtensionManager::set(ExtensionEntry {
|
||||
enabled: extension_query.enabled,
|
||||
config: extension_query.config,
|
||||
}) {
|
||||
Ok(_) => Ok(Json(format!("Added extension {}", extension_query.name))),
|
||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/config/extension",
|
||||
request_body = ConfigKeyQuery,
|
||||
path = "/config/extensions/{name}",
|
||||
responses(
|
||||
(status = 200, description = "Extension removed successfully", body = String),
|
||||
(status = 404, description = "Extension not found"),
|
||||
@@ -206,31 +222,98 @@ pub async fn add_extension(
|
||||
pub async fn remove_extension(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Json(query): Json<ConfigKeyQuery>,
|
||||
axum::extract::Path(name): axum::extract::Path<String>,
|
||||
) -> Result<Json<String>, StatusCode> {
|
||||
// Use the helper function to verify the secret key
|
||||
verify_secret_key(&headers, &state)?;
|
||||
|
||||
let config = Config::global();
|
||||
let key = name_to_key(&name);
|
||||
// Use ExtensionManager to remove the extension
|
||||
match ExtensionManager::remove(&key) {
|
||||
Ok(_) => Ok(Json(format!("Removed extension {}", name))),
|
||||
Err(_) => Err(StatusCode::NOT_FOUND),
|
||||
}
|
||||
}
|
||||
|
||||
// Get current extensions
|
||||
let mut extensions: HashMap<String, Value> = match config.get_param("extensions") {
|
||||
Ok(exts) => exts,
|
||||
Err(_) => return Err(StatusCode::NOT_FOUND),
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/config/extensions/{name}",
|
||||
request_body = ExtensionQuery,
|
||||
responses(
|
||||
(status = 200, description = "Extension updated successfully", body = String),
|
||||
(status = 404, description = "Extension not found"),
|
||||
(status = 500, description = "Internal server error")
|
||||
)
|
||||
)]
|
||||
pub async fn update_extension(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
axum::extract::Path(name): axum::extract::Path<String>,
|
||||
Json(extension_query): Json<ExtensionQuery>,
|
||||
) -> Result<Json<String>, StatusCode> {
|
||||
verify_secret_key(&headers, &state)?;
|
||||
|
||||
let key = name_to_key(&name);
|
||||
|
||||
// Check if extension exists
|
||||
let extensions = ExtensionManager::get_all().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
if !extensions.iter().any(|entry| entry.config.key() == key) {
|
||||
return Err(StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
// Use ExtensionManager to update the extension
|
||||
match ExtensionManager::set(ExtensionEntry {
|
||||
enabled: extension_query.enabled,
|
||||
config: extension_query.config,
|
||||
}) {
|
||||
Ok(_) => Ok(Json(format!("Updated extension {}", extension_query.name))),
|
||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/extensions/{name}/toggle",
|
||||
responses(
|
||||
(status = 200, description = "Extension toggled successfully", body = String),
|
||||
(status = 404, description = "Extension not found"),
|
||||
(status = 500, description = "Internal server error")
|
||||
)
|
||||
)]
|
||||
pub async fn toggle_extension(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
axum::extract::Path(name): axum::extract::Path<String>,
|
||||
) -> Result<Json<String>, StatusCode> {
|
||||
verify_secret_key(&headers, &state)?;
|
||||
|
||||
let key = name_to_key(&name);
|
||||
|
||||
// Get the extension
|
||||
let extensions = ExtensionManager::get_all().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let extension = extensions
|
||||
.iter()
|
||||
.find(|e| e.config.key() == key)
|
||||
.ok_or(StatusCode::NOT_FOUND)?;
|
||||
|
||||
// Create a new entry with toggled enabled state
|
||||
let updated_entry = ExtensionEntry {
|
||||
enabled: !extension.enabled,
|
||||
config: extension.config.clone(),
|
||||
};
|
||||
|
||||
// Remove extension if it exists
|
||||
if extensions.remove(&query.key).is_some() {
|
||||
// Save updated extensions
|
||||
match config.set_param(
|
||||
"extensions",
|
||||
Value::Object(extensions.into_iter().collect()),
|
||||
) {
|
||||
Ok(_) => Ok(Json(format!("Removed extension {}", query.key))),
|
||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
// Update using ExtensionManager
|
||||
match ExtensionManager::set(updated_entry) {
|
||||
Ok(_) => {
|
||||
let status = if !extension.enabled {
|
||||
"enabled"
|
||||
} else {
|
||||
"disabled"
|
||||
};
|
||||
Ok(Json(format!("Extension {} {}", name, status)))
|
||||
}
|
||||
} else {
|
||||
Err(StatusCode::NOT_FOUND)
|
||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,50 +339,6 @@ pub async fn read_all_config(
|
||||
Ok(Json(ConfigResponse { config: values }))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/config/extension",
|
||||
request_body = ExtensionQuery,
|
||||
responses(
|
||||
(status = 200, description = "Extension configuration updated successfully", body = String),
|
||||
(status = 404, description = "Extension not found"),
|
||||
(status = 500, description = "Internal server error")
|
||||
)
|
||||
)]
|
||||
pub async fn update_extension(
|
||||
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_param("extensions") {
|
||||
Ok(exts) => exts,
|
||||
Err(_) => return Err(StatusCode::NOT_FOUND),
|
||||
};
|
||||
|
||||
// Check if extension exists
|
||||
if !extensions.contains_key(&extension.name) {
|
||||
return Err(StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
// Update extension configuration
|
||||
extensions.insert(extension.name.clone(), extension.config);
|
||||
|
||||
// Save updated extensions
|
||||
match config.set_param(
|
||||
"extensions",
|
||||
Value::Object(extensions.into_iter().collect()),
|
||||
) {
|
||||
Ok(_) => Ok(Json(format!("Updated extension {}", extension.name))),
|
||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
}
|
||||
}
|
||||
|
||||
// Modified providers function using the new response type
|
||||
#[utoipa::path(
|
||||
get,
|
||||
@@ -341,9 +380,11 @@ pub fn routes(state: AppState) -> Router {
|
||||
.route("/config/upsert", post(upsert_config))
|
||||
.route("/config/remove", post(remove_config))
|
||||
.route("/config/read", post(read_config))
|
||||
.route("/config/extension", post(add_extension))
|
||||
.route("/config/extension", put(update_extension))
|
||||
.route("/config/extension", delete(remove_extension))
|
||||
.route("/config/extensions", get(get_extensions))
|
||||
.route("/config/extensions", post(add_extension))
|
||||
.route("/config/extensions/:name", put(update_extension))
|
||||
.route("/config/extensions/:name", delete(remove_extension))
|
||||
.route("/extensions/:name/toggle", post(toggle_extension))
|
||||
.route("/config/providers", get(providers))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ serde_yaml = "0.9.34"
|
||||
once_cell = "1.20.2"
|
||||
etcetera = "0.8.0"
|
||||
rand = "0.8.5"
|
||||
utoipa = { version = "4.1" }
|
||||
utoipa = "4.1"
|
||||
|
||||
# For Bedrock provider
|
||||
aws-config = { version = "1.1.7", features = ["behavior-version-latest"] }
|
||||
|
||||
@@ -171,7 +171,7 @@ impl Capabilities {
|
||||
.await
|
||||
.map_err(|e| ExtensionError::Initialization(config.clone(), e))?;
|
||||
|
||||
let sanitized_name = normalize(config.name().to_string());
|
||||
let sanitized_name = normalize(config.key().to_string());
|
||||
|
||||
// Store instructions if provided
|
||||
if let Some(instructions) = init_result.instructions {
|
||||
|
||||
@@ -3,8 +3,10 @@ use std::collections::HashMap;
|
||||
use mcp_client::client::Error as ClientError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::config;
|
||||
use crate::config::extensions::name_to_key;
|
||||
|
||||
/// Errors from Extension operation
|
||||
#[derive(Error, Debug)]
|
||||
@@ -21,7 +23,7 @@ pub enum ExtensionError {
|
||||
|
||||
pub type ExtensionResult<T> = Result<T, ExtensionError>;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default, ToSchema)]
|
||||
pub struct Envs {
|
||||
/// A map of environment variables to set, e.g. API_KEY -> some_secret, HOST -> host
|
||||
#[serde(default)]
|
||||
@@ -43,7 +45,7 @@ impl Envs {
|
||||
}
|
||||
|
||||
/// Represents the different types of MCP extensions that can be added to the manager
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ExtensionConfig {
|
||||
/// Server-sent events client with a URI endpoint
|
||||
@@ -130,13 +132,19 @@ impl ExtensionConfig {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn key(&self) -> String {
|
||||
let name = self.name();
|
||||
name_to_key(&name)
|
||||
}
|
||||
|
||||
/// Get the extension name regardless of variant
|
||||
pub fn name(&self) -> &str {
|
||||
pub fn name(&self) -> String {
|
||||
match self {
|
||||
Self::Sse { name, .. } => name,
|
||||
Self::Stdio { name, .. } => name,
|
||||
Self::Builtin { name, .. } => name,
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,23 +3,28 @@ use crate::agents::ExtensionConfig;
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const DEFAULT_EXTENSION: &str = "developer";
|
||||
pub const DEFAULT_EXTENSION_TIMEOUT: u64 = 300;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)]
|
||||
pub struct ExtensionEntry {
|
||||
pub enabled: bool,
|
||||
#[serde(flatten)]
|
||||
pub config: ExtensionConfig,
|
||||
}
|
||||
|
||||
pub fn name_to_key(name: &str) -> String {
|
||||
name.to_string()
|
||||
}
|
||||
|
||||
/// Extension configuration management
|
||||
pub struct ExtensionManager;
|
||||
|
||||
impl ExtensionManager {
|
||||
/// Get the extension configuration if enabled
|
||||
pub fn get_config(name: &str) -> Result<Option<ExtensionConfig>> {
|
||||
/// Get the extension configuration if enabled -- uses key
|
||||
pub fn get_config(key: &str) -> Result<Option<ExtensionConfig>> {
|
||||
let config = Config::global();
|
||||
|
||||
// Try to get the extension entry
|
||||
@@ -28,7 +33,7 @@ impl ExtensionManager {
|
||||
Err(super::ConfigError::NotFound(_)) => {
|
||||
// Initialize with default developer extension
|
||||
let defaults = HashMap::from([(
|
||||
DEFAULT_EXTENSION.to_string(),
|
||||
name_to_key(DEFAULT_EXTENSION), // Use key format for top-level key in config
|
||||
ExtensionEntry {
|
||||
enabled: true,
|
||||
config: ExtensionConfig::Builtin {
|
||||
@@ -43,7 +48,7 @@ impl ExtensionManager {
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
|
||||
Ok(extensions.get(name).and_then(|entry| {
|
||||
Ok(extensions.get(key).and_then(|entry| {
|
||||
if entry.enabled {
|
||||
Some(entry.config.clone())
|
||||
} else {
|
||||
@@ -60,33 +65,35 @@ impl ExtensionManager {
|
||||
.get_param("extensions")
|
||||
.unwrap_or_else(|_| HashMap::new());
|
||||
|
||||
extensions.insert(entry.config.name().parse()?, entry);
|
||||
let key = entry.config.key();
|
||||
|
||||
extensions.insert(key, entry);
|
||||
config.set_param("extensions", serde_json::to_value(extensions)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove an extension configuration
|
||||
pub fn remove(name: &str) -> Result<()> {
|
||||
/// Remove an extension configuration -- uses the key
|
||||
pub fn remove(key: &str) -> Result<()> {
|
||||
let config = Config::global();
|
||||
|
||||
let mut extensions: HashMap<String, ExtensionEntry> = config
|
||||
.get_param("extensions")
|
||||
.unwrap_or_else(|_| HashMap::new());
|
||||
|
||||
extensions.remove(name);
|
||||
extensions.remove(key);
|
||||
config.set_param("extensions", serde_json::to_value(extensions)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Enable or disable an extension
|
||||
pub fn set_enabled(name: &str, enabled: bool) -> Result<()> {
|
||||
/// Enable or disable an extension -- uses key
|
||||
pub fn set_enabled(key: &str, enabled: bool) -> Result<()> {
|
||||
let config = Config::global();
|
||||
|
||||
let mut extensions: HashMap<String, ExtensionEntry> = config
|
||||
.get_param("extensions")
|
||||
.unwrap_or_else(|_| HashMap::new());
|
||||
|
||||
if let Some(entry) = extensions.get_mut(name) {
|
||||
if let Some(entry) = extensions.get_mut(key) {
|
||||
entry.enabled = enabled;
|
||||
config.set_param("extensions", serde_json::to_value(extensions)?)?;
|
||||
}
|
||||
@@ -109,16 +116,17 @@ impl ExtensionManager {
|
||||
.unwrap_or_else(|_| get_keys(Default::default())))
|
||||
}
|
||||
|
||||
/// Check if an extension is enabled
|
||||
pub fn is_enabled(name: &str) -> Result<bool> {
|
||||
/// Check if an extension is enabled - FIXED to use key
|
||||
pub fn is_enabled(key: &str) -> Result<bool> {
|
||||
let config = Config::global();
|
||||
let extensions: HashMap<String, ExtensionEntry> = config
|
||||
.get_param("extensions")
|
||||
.unwrap_or_else(|_| HashMap::new());
|
||||
|
||||
Ok(extensions.get(name).map(|e| e.enabled).unwrap_or(false))
|
||||
Ok(extensions.get(key).map(|e| e.enabled).unwrap_or(false))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_keys(entries: HashMap<String, ExtensionEntry>) -> Vec<String> {
|
||||
entries.into_keys().collect()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
mod base;
|
||||
mod experiments;
|
||||
mod extensions;
|
||||
pub mod extensions;
|
||||
|
||||
pub use crate::agents::ExtensionConfig;
|
||||
pub use base::{Config, ConfigError, APP_STRATEGY};
|
||||
|
||||
+235
-14
@@ -33,7 +33,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/config/extension": {
|
||||
"/config/extensions": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"super::routes::config_management"
|
||||
],
|
||||
"operationId": "get_extensions",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "All extensions retrieved successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ExtensionResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"super::routes::config_management"
|
||||
@@ -67,12 +88,24 @@
|
||||
"description": "Internal server error"
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
"/config/extensions/{name}": {
|
||||
"put": {
|
||||
"tags": [
|
||||
"super::routes::config_management"
|
||||
],
|
||||
"operationId": "update_extension",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
@@ -85,7 +118,7 @@
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Extension configuration updated successfully",
|
||||
"description": "Extension updated successfully",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
@@ -107,16 +140,16 @@
|
||||
"super::routes::config_management"
|
||||
],
|
||||
"operationId": "remove_extension",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ConfigKeyQuery"
|
||||
}
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Extension removed successfully",
|
||||
@@ -259,6 +292,42 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/extensions/{name}/toggle": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"super::routes::config_management"
|
||||
],
|
||||
"operationId": "toggle_extension",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Extension toggled successfully",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Extension not found"
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
@@ -313,19 +382,171 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Envs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"description": "A map of environment variables to set, e.g. API_KEY -> some_secret, HOST -> host"
|
||||
}
|
||||
},
|
||||
"ExtensionConfig": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"description": "Server-sent events client with a URI endpoint",
|
||||
"required": [
|
||||
"name",
|
||||
"uri",
|
||||
"type"
|
||||
],
|
||||
"properties": {
|
||||
"envs": {
|
||||
"$ref": "#/components/schemas/Envs"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name used to identify this extension"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"nullable": true,
|
||||
"minimum": 0
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"sse"
|
||||
]
|
||||
},
|
||||
"uri": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"description": "Standard I/O client with command and arguments",
|
||||
"required": [
|
||||
"name",
|
||||
"cmd",
|
||||
"args",
|
||||
"type"
|
||||
],
|
||||
"properties": {
|
||||
"args": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"cmd": {
|
||||
"type": "string"
|
||||
},
|
||||
"envs": {
|
||||
"$ref": "#/components/schemas/Envs"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name used to identify this extension"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"nullable": true,
|
||||
"minimum": 0
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"stdio"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"description": "Built-in extension that is part of the goose binary",
|
||||
"required": [
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name used to identify this extension"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"nullable": true,
|
||||
"minimum": 0
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"builtin"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"description": "Represents the different types of MCP extensions that can be added to the manager",
|
||||
"discriminator": {
|
||||
"propertyName": "type"
|
||||
}
|
||||
},
|
||||
"ExtensionEntry": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ExtensionConfig"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ExtensionQuery": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"config"
|
||||
"config",
|
||||
"enabled"
|
||||
],
|
||||
"properties": {
|
||||
"config": {},
|
||||
"config": {
|
||||
"$ref": "#/components/schemas/ExtensionConfig"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ExtensionResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"extensions"
|
||||
],
|
||||
"properties": {
|
||||
"extensions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ExtensionEntry"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ProviderDetails": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import type { Options as ClientOptions, TDataShape, Client } from '@hey-api/client-fetch';
|
||||
import type { ReadAllConfigData, ReadAllConfigResponse, RemoveExtensionData, RemoveExtensionResponse, AddExtensionData, AddExtensionResponse, UpdateExtensionData, UpdateExtensionResponse, ProvidersData, ProvidersResponse2, ReadConfigData, RemoveConfigData, RemoveConfigResponse, UpsertConfigData, UpsertConfigResponse } from './types.gen';
|
||||
import type { ReadAllConfigData, ReadAllConfigResponse, GetExtensionsData, GetExtensionsResponse, AddExtensionData, AddExtensionResponse, RemoveExtensionData, RemoveExtensionResponse, UpdateExtensionData, UpdateExtensionResponse, ProvidersData, ProvidersResponse2, ReadConfigData, RemoveConfigData, RemoveConfigResponse, UpsertConfigData, UpsertConfigResponse, ToggleExtensionData, ToggleExtensionResponse } from './types.gen';
|
||||
import { client as _heyApiClient } from './client.gen';
|
||||
|
||||
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<TData, ThrowOnError> & {
|
||||
@@ -25,20 +25,16 @@ export const readAllConfig = <ThrowOnError extends boolean = false>(options?: Op
|
||||
});
|
||||
};
|
||||
|
||||
export const removeExtension = <ThrowOnError extends boolean = false>(options: Options<RemoveExtensionData, ThrowOnError>) => {
|
||||
return (options.client ?? _heyApiClient).delete<RemoveExtensionResponse, unknown, ThrowOnError>({
|
||||
url: '/config/extension',
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers
|
||||
}
|
||||
export const getExtensions = <ThrowOnError extends boolean = false>(options?: Options<GetExtensionsData, ThrowOnError>) => {
|
||||
return (options?.client ?? _heyApiClient).get<GetExtensionsResponse, unknown, ThrowOnError>({
|
||||
url: '/config/extensions',
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
export const addExtension = <ThrowOnError extends boolean = false>(options: Options<AddExtensionData, ThrowOnError>) => {
|
||||
return (options.client ?? _heyApiClient).post<AddExtensionResponse, unknown, ThrowOnError>({
|
||||
url: '/config/extension',
|
||||
url: '/config/extensions',
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -47,9 +43,16 @@ export const addExtension = <ThrowOnError extends boolean = false>(options: Opti
|
||||
});
|
||||
};
|
||||
|
||||
export const removeExtension = <ThrowOnError extends boolean = false>(options: Options<RemoveExtensionData, ThrowOnError>) => {
|
||||
return (options.client ?? _heyApiClient).delete<RemoveExtensionResponse, unknown, ThrowOnError>({
|
||||
url: '/config/extensions/{name}',
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
export const updateExtension = <ThrowOnError extends boolean = false>(options: Options<UpdateExtensionData, ThrowOnError>) => {
|
||||
return (options.client ?? _heyApiClient).put<UpdateExtensionResponse, unknown, ThrowOnError>({
|
||||
url: '/config/extension',
|
||||
url: '/config/extensions/{name}',
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -96,4 +99,11 @@ export const upsertConfig = <ThrowOnError extends boolean = false>(options: Opti
|
||||
...options?.headers
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const toggleExtension = <ThrowOnError extends boolean = false>(options: Options<ToggleExtensionData, ThrowOnError>) => {
|
||||
return (options.client ?? _heyApiClient).post<ToggleExtensionResponse, unknown, ThrowOnError>({
|
||||
url: '/extensions/{name}/toggle',
|
||||
...options
|
||||
});
|
||||
};
|
||||
+120
-18
@@ -16,9 +16,55 @@ export type ConfigResponse = {
|
||||
config: {};
|
||||
};
|
||||
|
||||
export type ExtensionQuery = {
|
||||
config: unknown;
|
||||
export type Envs = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents the different types of MCP extensions that can be added to the manager
|
||||
*/
|
||||
export type ExtensionConfig = {
|
||||
envs?: Envs;
|
||||
/**
|
||||
* The name used to identify this extension
|
||||
*/
|
||||
name: string;
|
||||
timeout?: number | null;
|
||||
type: 'sse';
|
||||
uri: string;
|
||||
} | {
|
||||
args: Array<string>;
|
||||
cmd: string;
|
||||
envs?: Envs;
|
||||
/**
|
||||
* The name used to identify this extension
|
||||
*/
|
||||
name: string;
|
||||
timeout?: number | null;
|
||||
type: 'stdio';
|
||||
} | {
|
||||
/**
|
||||
* The name used to identify this extension
|
||||
*/
|
||||
name: string;
|
||||
timeout?: number | null;
|
||||
type: 'builtin';
|
||||
};
|
||||
|
||||
export type ExtensionEntry = ExtensionConfig & {
|
||||
type?: 'ExtensionEntry';
|
||||
} & {
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export type ExtensionQuery = {
|
||||
config: ExtensionConfig;
|
||||
enabled: boolean;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type ExtensionResponse = {
|
||||
extensions: Array<ExtensionEntry>;
|
||||
};
|
||||
|
||||
export type ProviderDetails = {
|
||||
@@ -94,38 +140,34 @@ export type ReadAllConfigResponses = {
|
||||
|
||||
export type ReadAllConfigResponse = ReadAllConfigResponses[keyof ReadAllConfigResponses];
|
||||
|
||||
export type RemoveExtensionData = {
|
||||
body: ConfigKeyQuery;
|
||||
export type GetExtensionsData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/config/extension';
|
||||
url: '/config/extensions';
|
||||
};
|
||||
|
||||
export type RemoveExtensionErrors = {
|
||||
/**
|
||||
* Extension not found
|
||||
*/
|
||||
404: unknown;
|
||||
export type GetExtensionsErrors = {
|
||||
/**
|
||||
* Internal server error
|
||||
*/
|
||||
500: unknown;
|
||||
};
|
||||
|
||||
export type RemoveExtensionResponses = {
|
||||
export type GetExtensionsResponses = {
|
||||
/**
|
||||
* Extension removed successfully
|
||||
* All extensions retrieved successfully
|
||||
*/
|
||||
200: string;
|
||||
200: ExtensionResponse;
|
||||
};
|
||||
|
||||
export type RemoveExtensionResponse = RemoveExtensionResponses[keyof RemoveExtensionResponses];
|
||||
export type GetExtensionsResponse = GetExtensionsResponses[keyof GetExtensionsResponses];
|
||||
|
||||
export type AddExtensionData = {
|
||||
body: ExtensionQuery;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/config/extension';
|
||||
url: '/config/extensions';
|
||||
};
|
||||
|
||||
export type AddExtensionErrors = {
|
||||
@@ -148,11 +190,42 @@ export type AddExtensionResponses = {
|
||||
|
||||
export type AddExtensionResponse = AddExtensionResponses[keyof AddExtensionResponses];
|
||||
|
||||
export type RemoveExtensionData = {
|
||||
body?: never;
|
||||
path: {
|
||||
name: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/config/extensions/{name}';
|
||||
};
|
||||
|
||||
export type RemoveExtensionErrors = {
|
||||
/**
|
||||
* Extension not found
|
||||
*/
|
||||
404: unknown;
|
||||
/**
|
||||
* Internal server error
|
||||
*/
|
||||
500: unknown;
|
||||
};
|
||||
|
||||
export type RemoveExtensionResponses = {
|
||||
/**
|
||||
* Extension removed successfully
|
||||
*/
|
||||
200: string;
|
||||
};
|
||||
|
||||
export type RemoveExtensionResponse = RemoveExtensionResponses[keyof RemoveExtensionResponses];
|
||||
|
||||
export type UpdateExtensionData = {
|
||||
body: ExtensionQuery;
|
||||
path?: never;
|
||||
path: {
|
||||
name: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/config/extension';
|
||||
url: '/config/extensions/{name}';
|
||||
};
|
||||
|
||||
export type UpdateExtensionErrors = {
|
||||
@@ -168,7 +241,7 @@ export type UpdateExtensionErrors = {
|
||||
|
||||
export type UpdateExtensionResponses = {
|
||||
/**
|
||||
* Extension configuration updated successfully
|
||||
* Extension updated successfully
|
||||
*/
|
||||
200: string;
|
||||
};
|
||||
@@ -262,6 +335,35 @@ export type UpsertConfigResponses = {
|
||||
|
||||
export type UpsertConfigResponse = UpsertConfigResponses[keyof UpsertConfigResponses];
|
||||
|
||||
export type ToggleExtensionData = {
|
||||
body?: never;
|
||||
path: {
|
||||
name: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/extensions/{name}/toggle';
|
||||
};
|
||||
|
||||
export type ToggleExtensionErrors = {
|
||||
/**
|
||||
* Extension not found
|
||||
*/
|
||||
404: unknown;
|
||||
/**
|
||||
* Internal server error
|
||||
*/
|
||||
500: unknown;
|
||||
};
|
||||
|
||||
export type ToggleExtensionResponses = {
|
||||
/**
|
||||
* Extension toggled successfully
|
||||
*/
|
||||
200: string;
|
||||
};
|
||||
|
||||
export type ToggleExtensionResponse = ToggleExtensionResponses[keyof ToggleExtensionResponses];
|
||||
|
||||
export type ClientOptions = {
|
||||
baseUrl: `${string}://${string}` | (string & {});
|
||||
};
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
readConfig,
|
||||
removeConfig,
|
||||
upsertConfig,
|
||||
getExtensions as apiGetExtensions,
|
||||
toggleExtension as apiToggleExtension,
|
||||
addExtension as apiAddExtension,
|
||||
removeExtension as apiRemoveExtension,
|
||||
updateExtension as apiUpdateExtension,
|
||||
@@ -14,8 +16,11 @@ import type {
|
||||
ConfigResponse,
|
||||
UpsertConfigQuery,
|
||||
ConfigKeyQuery,
|
||||
ExtensionQuery,
|
||||
ExtensionResponse,
|
||||
ExtensionEntry,
|
||||
ProviderDetails,
|
||||
ExtensionQuery,
|
||||
ExtensionConfig,
|
||||
} from '../api/types.gen';
|
||||
|
||||
// Initialize client configuration
|
||||
@@ -33,10 +38,12 @@ interface ConfigContextType {
|
||||
upsert: (key: string, value: unknown, is_secret: boolean) => Promise<void>;
|
||||
read: (key: string, is_secret: boolean) => Promise<unknown>;
|
||||
remove: (key: string, is_secret: boolean) => Promise<void>;
|
||||
addExtension: (name: string, config: unknown) => Promise<void>;
|
||||
updateExtension: (name: string, config: unknown) => Promise<void>;
|
||||
addExtension: (name: string, config: ExtensionConfig, enabled: boolean) => Promise<void>;
|
||||
updateExtension: (name: string, config: ExtensionConfig, enabled: boolean) => Promise<void>;
|
||||
toggleExtension: (name: string) => Promise<void>;
|
||||
removeExtension: (name: string) => Promise<void>;
|
||||
getProviders: (b: boolean) => Promise<ProviderDetails[]>;
|
||||
getExtensions: (b: boolean) => Promise<ExtensionEntry[]>
|
||||
}
|
||||
|
||||
interface ConfigProviderProps {
|
||||
@@ -48,6 +55,7 @@ const ConfigContext = createContext<ConfigContextType | undefined>(undefined);
|
||||
export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
||||
const [config, setConfig] = useState<ConfigResponse['config']>({});
|
||||
const [providersList, setProvidersList] = useState<ProviderDetails[]>([]);
|
||||
const [extensionsList, setExtensionsList] = useState<ExtensionEntry[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
// Load all configuration data and providers on mount
|
||||
@@ -63,6 +71,15 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
||||
} catch (error) {
|
||||
console.error('Failed to load providers:', error);
|
||||
}
|
||||
|
||||
// Load extensions
|
||||
try {
|
||||
const extensionsResponse = await apiGetExtensions()
|
||||
setExtensionsList(extensionsResponse.data.extensions)
|
||||
} catch (error) {
|
||||
console.error('Failed to load extensions:', error)
|
||||
}
|
||||
|
||||
})();
|
||||
}, []);
|
||||
|
||||
@@ -99,8 +116,8 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
||||
await reloadConfig();
|
||||
};
|
||||
|
||||
const addExtension = async (name: string, config: unknown) => {
|
||||
const query: ExtensionQuery = { name, config };
|
||||
const addExtension = async (name: string, config: ExtensionConfig, enabled: boolean) => {
|
||||
const query: ExtensionQuery = { name, config, enabled };
|
||||
await apiAddExtension({
|
||||
body: query,
|
||||
});
|
||||
@@ -108,18 +125,21 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
||||
};
|
||||
|
||||
const removeExtension = async (name: string) => {
|
||||
const query: ConfigKeyQuery = { key: name, is_secret: false };
|
||||
await apiRemoveExtension({
|
||||
await apiRemoveExtension({path: {name: name}});
|
||||
await reloadConfig();
|
||||
};
|
||||
|
||||
const updateExtension = async (name: string, config: ExtensionConfig, enabled: boolean) => {
|
||||
const query: ExtensionQuery = { name, config, enabled };
|
||||
await apiUpdateExtension({
|
||||
body: query,
|
||||
path: {name: name}
|
||||
});
|
||||
await reloadConfig();
|
||||
};
|
||||
|
||||
const updateExtension = async (name: string, config: unknown) => {
|
||||
const query: ExtensionQuery = { name, config };
|
||||
await apiUpdateExtension({
|
||||
body: query,
|
||||
});
|
||||
const toggleExtension = async (name: string) => {
|
||||
await apiToggleExtension({path: {name: name}});
|
||||
await reloadConfig();
|
||||
};
|
||||
|
||||
@@ -134,6 +154,18 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
||||
return providersList;
|
||||
};
|
||||
|
||||
const getExtensions = async (forceRefresh = false): Promise<ExtensionEntry[]> => {
|
||||
if (forceRefresh || extensionsList.length === 0) {
|
||||
// If a refresh is forced, or we don't have providers yet
|
||||
const response = await apiGetExtensions();
|
||||
const extensionResponse: ExtensionResponse = response.data
|
||||
setExtensionsList(extensionResponse.extensions);
|
||||
return extensionResponse.extensions;
|
||||
}
|
||||
// Otherwise return the cached providers
|
||||
return extensionsList;
|
||||
};
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
config,
|
||||
@@ -144,7 +176,9 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
||||
addExtension,
|
||||
updateExtension,
|
||||
removeExtension,
|
||||
toggleExtension,
|
||||
getProviders,
|
||||
getExtensions,
|
||||
}),
|
||||
[config, providersList]
|
||||
); // Functions don't need to be dependencies as they don't change
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useConfig } from '../ConfigContext';
|
||||
import { Button } from '../ui/button';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { Gear } from '../icons/Gear';
|
||||
import ExtensionsSection from './ExtensionsSection';
|
||||
import ExtensionsSection from './extensions/ExtensionsSection';
|
||||
|
||||
interface ModelOption {
|
||||
id: string;
|
||||
|
||||
+14
-13
@@ -1,14 +1,14 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button } from '../ui/button';
|
||||
import { Switch } from '../ui/switch';
|
||||
import { Button } from '../../ui/button';
|
||||
import { Switch } from '../../ui/switch';
|
||||
import { Plus, X } from 'lucide-react';
|
||||
import { Gear } from '../icons/Gear';
|
||||
import { GPSIcon } from '../ui/icons';
|
||||
import { useConfig } from '../ConfigContext';
|
||||
import Modal from '../Modal';
|
||||
import { Input } from '../ui/input';
|
||||
import { Gear } from '../../icons/Gear';
|
||||
import { GPSIcon } from '../../ui/icons';
|
||||
import { useConfig } from '../../ConfigContext';
|
||||
import Modal from '../../Modal';
|
||||
import { Input } from '../../ui/input';
|
||||
import Select from 'react-select';
|
||||
import { createDarkSelectStyles, darkSelectTheme } from '../ui/select-styles';
|
||||
import { createDarkSelectStyles, darkSelectTheme } from '../../ui/select-styles';
|
||||
|
||||
interface ExtensionConfig {
|
||||
args?: string[];
|
||||
@@ -16,7 +16,7 @@ interface ExtensionConfig {
|
||||
enabled: boolean;
|
||||
envs?: Record<string, string>;
|
||||
name: string;
|
||||
type: 'stdio' | 'sse';
|
||||
type: 'stdio' | 'sse' | 'builtin';
|
||||
}
|
||||
|
||||
interface ExtensionItem {
|
||||
@@ -50,7 +50,7 @@ const getSubtitle = (config: ExtensionConfig): string => {
|
||||
};
|
||||
|
||||
export default function ExtensionsSection() {
|
||||
const { config, updateExtension, addExtension } = useConfig();
|
||||
const { config, read, updateExtension, addExtension } = useConfig();
|
||||
const [extensions, setExtensions] = useState<ExtensionItem[]>([]);
|
||||
const [selectedExtension, setSelectedExtension] = useState<ExtensionItem | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
@@ -74,8 +74,9 @@ export default function ExtensionsSection() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (config.extensions) {
|
||||
const extensionItems: ExtensionItem[] = Object.entries(config.extensions).map(
|
||||
const extensions = read('extensions', false)
|
||||
if (extensions) {
|
||||
const extensionItems: ExtensionItem[] = Object.entries(extensions).map(
|
||||
([name, ext]) => {
|
||||
const extensionConfig = ext as ExtensionConfig;
|
||||
return {
|
||||
@@ -90,7 +91,7 @@ export default function ExtensionsSection() {
|
||||
);
|
||||
setExtensions(extensionItems);
|
||||
}
|
||||
}, [config.extensions]);
|
||||
}, [read]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedExtension) {
|
||||
@@ -0,0 +1,121 @@
|
||||
// TODO: copied this from old code
|
||||
|
||||
// import {View} from '../../../App'
|
||||
// import {SettingsViewOptions} from "../SettingsView";
|
||||
// import {toast} from "react-toastify";
|
||||
//
|
||||
//
|
||||
// export async function addExtensionFromDeepLink(
|
||||
// url: string,
|
||||
// setView: (view: View, options: SettingsViewOptions) => void
|
||||
// ) {
|
||||
// if (!url.startsWith('goose://extension')) {
|
||||
// handleError(
|
||||
// 'Failed to install extension: Invalid URL: URL must use the goose://extension scheme'
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// const parsedUrl = new URL(url);
|
||||
//
|
||||
// if (parsedUrl.protocol !== 'goose:') {
|
||||
// handleError(
|
||||
// 'Failed to install extension: Invalid protocol: URL must use the goose:// scheme',
|
||||
// true
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// // Check that all required fields are present and not empty
|
||||
// const requiredFields = ['name', 'description'];
|
||||
//
|
||||
// for (const field of requiredFields) {
|
||||
// const value = parsedUrl.searchParams.get(field);
|
||||
// if (!value || value.trim() === '') {
|
||||
// handleError(
|
||||
// `Failed to install extension: The link is missing required field '${field}'`,
|
||||
// true
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// const cmd = parsedUrl.searchParams.get('cmd');
|
||||
// if (!cmd) {
|
||||
// handleError("Failed to install extension: Missing required 'cmd' parameter in the URL", true);
|
||||
// }
|
||||
//
|
||||
// // Validate that the command is one of the allowed commands
|
||||
// const allowedCommands = ['npx', 'uvx', 'goosed'];
|
||||
// if (!allowedCommands.includes(cmd)) {
|
||||
// handleError(
|
||||
// `Failed to install extension: Invalid command: ${cmd}. Only ${allowedCommands.join(', ')} are allowed.`,
|
||||
// true
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// // Check for security risk with npx -c command
|
||||
// const args = parsedUrl.searchParams.getAll('arg');
|
||||
// if (cmd === 'npx' && args.includes('-c')) {
|
||||
// handleError(
|
||||
// 'Failed to install extension: npx with -c argument can lead to code injection',
|
||||
// true
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// const envList = parsedUrl.searchParams.getAll('env');
|
||||
// const id = parsedUrl.searchParams.get('id');
|
||||
// const name = parsedUrl.searchParams.get('name');
|
||||
// const description = parsedUrl.searchParams.get('description');
|
||||
// const timeout = parsedUrl.searchParams.get('timeout');
|
||||
//
|
||||
// // split env based on delimiter to a map
|
||||
// const envs = envList.reduce(
|
||||
// (acc, env) => {
|
||||
// const [key, value] = env.split('=');
|
||||
// acc[key] = value;
|
||||
// return acc;
|
||||
// },
|
||||
// {} as Record<string, string>
|
||||
// );
|
||||
//
|
||||
// // Create a ExtensionConfig from the URL parameters
|
||||
// // Parse timeout if provided, otherwise use default
|
||||
// const parsedTimeout = timeout ? parseInt(timeout, 10) : null;
|
||||
//
|
||||
// const extensionConfig: ExtensionConfig = {
|
||||
// id,
|
||||
// name,
|
||||
// type: 'stdio',
|
||||
// cmd,
|
||||
// args,
|
||||
// description,
|
||||
// enabled: true,
|
||||
// env_keys: Object.keys(envs).length > 0 ? Object.keys(envs) : [],
|
||||
// timeout:
|
||||
// parsedTimeout !== null && !isNaN(parsedTimeout) && Number.isInteger(parsedTimeout)
|
||||
// ? parsedTimeout
|
||||
// : DEFAULT_EXTENSION_TIMEOUT,
|
||||
// };
|
||||
//
|
||||
// // Store the extension config regardless of env vars status
|
||||
// storeExtensionConfig(extensionConfig);
|
||||
//
|
||||
// // Check if extension requires env vars and go to settings if so
|
||||
// if (envVarsRequired(extensionConfig)) {
|
||||
// console.log('Environment variables required, redirecting to settings');
|
||||
// setView('settings', { extensionId: extensionConfig.id, showEnvVars: true });
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // If no env vars are required, proceed with extending Goosed
|
||||
// await addExtension(extensionConfig);
|
||||
// }
|
||||
//
|
||||
// function handleError(message: string, shouldThrow = false): void {
|
||||
// toast.error(message);
|
||||
// console.error(message);
|
||||
// if (shouldThrow) {
|
||||
// throw new Error(message);
|
||||
// }
|
||||
// }
|
||||
|
||||
// TODO: when rust app starts, add built-in extensions to config.yaml if they aren't there already
|
||||
Reference in New Issue
Block a user