diff --git a/.gitignore b/.gitignore index b82cf5c75309..fb5d128862cc 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ tokenizer_files/ .DS_Store .idea .vscode +.zed/ *.log tmp/ diff --git a/Cargo.lock b/Cargo.lock index d4a76d6a8ffc..d20de2b52269 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5070,17 +5070,14 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_yaml", - "socket2", "thiserror 2.0.18", "tokio", "tokio-stream", - "tokio-tungstenite 0.29.0", "tokio-util", "tower", "tower-http 0.7.0", "tracing", "tracing-subscriber", - "url", "utoipa 4.2.3", "uuid", "winreg", @@ -6453,9 +6450,9 @@ checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" [[package]] name = "lopdf" -version = "0.41.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67513274c50a2b51e5f75d9e682fcf4ab064a8a9c9ae2c3c59309084882bb24d" +checksum = "25aab26d99567469098e64a02f42679f8965c6401263eefa31d8f2dcc37a221c" dependencies = [ "aes", "bitflags 2.11.1", @@ -11771,13 +11768,7 @@ checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" dependencies = [ "futures-util", "log", - "native-tls", - "rustls", - "rustls-native-certs", - "rustls-pki-types", "tokio", - "tokio-native-tls", - "tokio-rustls", "tungstenite 0.29.0", ] @@ -12256,10 +12247,7 @@ dependencies = [ "http 1.4.2", "httparse", "log", - "native-tls", "rand 0.9.4", - "rustls", - "rustls-pki-types", "sha1", "thiserror 2.0.18", ] diff --git a/crates/goose-mcp/Cargo.toml b/crates/goose-mcp/Cargo.toml index 45ac8577f207..07335e821e22 100644 --- a/crates/goose-mcp/Cargo.toml +++ b/crates/goose-mcp/Cargo.toml @@ -35,7 +35,7 @@ etcetera = { workspace = true } tempfile = { workspace = true } include_dir = { workspace = true } once_cell = { workspace = true } -lopdf = { version = "0.41", default-features = false } +lopdf = { version = "0.42", default-features = false } docx-rs = { version = "0.4.18", default-features = false, features = ["image"] } image = { version = "0.24.4", default-features = false, features = ["bmp", "dds", "dxt", "farbfeld", "gif", "hdr", "ico", "jpeg", "jpeg_rayon", "openexr", "png", "pnm", "tga", "tiff", "webp"] } umya-spreadsheet = { version = "2", default-features = false } diff --git a/crates/goose-providers/src/formats/openai.rs b/crates/goose-providers/src/formats/openai.rs index 912622bf0bf0..4c4e8acc9ff4 100644 --- a/crates/goose-providers/src/formats/openai.rs +++ b/crates/goose-providers/src/formats/openai.rs @@ -1004,8 +1004,9 @@ where let mut tool_call_data: ToolCallData = HashMap::new(); if let Some(tool_calls) = &chunk.choices[0].delta.tool_calls { - for tool_call in tool_calls { - if let (Some(index), Some(id), Some(name)) = (tool_call.index, &tool_call.id, &tool_call.function.name) { + for (position, tool_call) in tool_calls.iter().enumerate() { + if let (Some(id), Some(name)) = (&tool_call.id, &tool_call.function.name) { + let index = tool_call.index.unwrap_or(position as i32); tool_call_data.insert(index, (id.clone(), name.clone(), tool_call.function.arguments.clone(), tool_call.extra.clone())); } } @@ -3652,6 +3653,66 @@ data: [DONE]"#; Ok(()) } + #[tokio::test] + async fn test_streaming_tool_call_without_tool_call_index() -> anyhow::Result<()> { + let response_lines = concat!( + "data: {\"id\":\"x\",\"object\":\"chat.completion.chunk\",\"model\":\"m\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"tool_calls\":[{\"id\":\"functions.get_weather:0\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\": \\\"Paris\\\"}\"}}]},\"finish_reason\":\"tool_calls\"}]}\n", + "data: [DONE]" + ); + let lines: Vec = response_lines.lines().map(|s| s.to_string()).collect(); + let response_stream = tokio_stream::iter(lines.into_iter().map(Ok)); + let mut messages = std::pin::pin!(response_to_streaming_message(response_stream)); + + let mut tool_calls = Vec::new(); + while let Some(result) = messages.next().await { + let (message, _usage) = result?; + if let Some(msg) = message { + for content in &msg.content { + if let MessageContent::ToolRequest(request) = content { + let tool_call = request.tool_call.as_ref().expect("tool call should parse"); + tool_calls.push((tool_call.name.to_string(), tool_call.arguments.clone())); + } + } + } + } + + assert_eq!(tool_calls.len(), 1); + assert_eq!(tool_calls[0].0, "get_weather"); + assert_eq!(tool_calls[0].1, Some(object!({"city": "Paris"}))); + Ok(()) + } + + #[tokio::test] + async fn test_streaming_multiple_tool_calls_without_tool_call_index() -> anyhow::Result<()> { + let response_lines = concat!( + "data: {\"id\":\"x\",\"object\":\"chat.completion.chunk\",\"model\":\"m\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"tool_calls\":[{\"id\":\"functions.get_weather:0\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\": \\\"Paris\\\"}\"}},{\"id\":\"functions.get_weather:1\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\": \\\"Tokyo\\\"}\"}}]},\"finish_reason\":\"tool_calls\"}]}\n", + "data: [DONE]" + ); + let lines: Vec = response_lines.lines().map(|s| s.to_string()).collect(); + let response_stream = tokio_stream::iter(lines.into_iter().map(Ok)); + let mut messages = std::pin::pin!(response_to_streaming_message(response_stream)); + + let mut tool_calls = Vec::new(); + while let Some(result) = messages.next().await { + let (message, _usage) = result?; + if let Some(msg) = message { + for content in &msg.content { + if let MessageContent::ToolRequest(request) = content { + let tool_call = request.tool_call.as_ref().expect("tool call should parse"); + tool_calls.push((tool_call.name.to_string(), tool_call.arguments.clone())); + } + } + } + } + + assert_eq!(tool_calls.len(), 2); + assert_eq!(tool_calls[0].0, "get_weather"); + assert_eq!(tool_calls[0].1, Some(object!({"city": "Paris"}))); + assert_eq!(tool_calls[1].0, "get_weather"); + assert_eq!(tool_calls[1].1, Some(object!({"city": "Tokyo"}))); + Ok(()) + } + // Streaming counterpart: both fields in one delta must parse and yield // thinking content, not fail with "duplicate field `reasoning_content`". #[tokio::test] diff --git a/crates/goose-sdk-types/src/custom_requests.rs b/crates/goose-sdk-types/src/custom_requests.rs index 1124d66cf320..e8143463c07e 100644 --- a/crates/goose-sdk-types/src/custom_requests.rs +++ b/crates/goose-sdk-types/src/custom_requests.rs @@ -51,13 +51,28 @@ pub struct RemoveSessionExtensionRequest { #[serde(rename_all = "camelCase")] pub struct GetToolsRequest { pub session_id: String, + /// Filter tools to those belonging to this extension. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub extension_name: Option, +} + +/// A single tool item returned by the tools list endpoint. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct ToolListItem { + pub name: String, + pub description: String, + pub parameters: Vec, + pub permission: Option, + pub input_schema: serde_json::Value, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub output_schema: Option, } /// Tools response. #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] pub struct GetToolsResponse { - /// Array of tool info objects with `name`, `description`, `parameters`, and optional `permission`. - pub tools: Vec, + pub tools: Vec, } /// Read a resource from an extension. @@ -231,6 +246,70 @@ pub struct DiagnosticsGetResponse { pub report: serde_json::Value, } +/// Information about a prompt template, including its default content and customization status. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct PromptTemplateEntry { + pub name: String, + pub description: String, + pub default_content: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub user_content: Option, + pub is_customized: bool, +} + +/// List all available Goose prompt templates. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/unstable/config/prompts/list", response = ListPromptsResponse)] +#[serde(rename_all = "camelCase")] +pub struct ListPromptsRequest {} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +#[serde(rename_all = "camelCase")] +pub struct ListPromptsResponse { + pub prompts: Vec, +} + +/// Read a Goose prompt template. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/unstable/config/prompts/get", response = GetPromptResponse)] +#[serde(rename_all = "camelCase")] +pub struct GetPromptRequest { + pub name: String, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +#[serde(rename_all = "camelCase")] +pub struct GetPromptResponse { + pub name: String, + pub content: String, + pub default_content: String, + pub is_customized: bool, +} + +/// Save a custom Goose prompt template. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/unstable/config/prompts/save", response = PromptOperationResponse)] +#[serde(rename_all = "camelCase")] +pub struct SavePromptRequest { + pub name: String, + pub content: String, +} + +/// Reset a Goose prompt template to its default content. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/unstable/config/prompts/reset", response = PromptOperationResponse)] +#[serde(rename_all = "camelCase")] +pub struct ResetPromptRequest { + pub name: String, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +#[serde(rename_all = "camelCase")] +pub struct PromptOperationResponse { + pub message: String, +} + /// Delete a session. #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] #[request(method = "session/delete", response = EmptyResponse)] @@ -400,6 +479,52 @@ pub struct PreferencesRemoveRequest { pub keys: Vec, } +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/unstable/config/read", response = ConfigReadResponse)] +#[serde(rename_all = "camelCase")] +pub struct ConfigReadRequest { + pub key: String, + #[serde(default)] + pub is_secret: bool, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +#[serde(rename_all = "camelCase")] +pub struct ConfigReadResponse { + #[serde(default)] + pub value: serde_json::Value, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/unstable/config/upsert", response = EmptyResponse)] +#[serde(rename_all = "camelCase")] +pub struct ConfigUpsertRequest { + pub key: String, + pub value: serde_json::Value, + #[serde(default)] + pub is_secret: bool, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/unstable/config/remove", response = EmptyResponse)] +#[serde(rename_all = "camelCase")] +pub struct ConfigRemoveRequest { + pub key: String, + #[serde(default)] + pub is_secret: bool, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/unstable/config/read-all", response = ConfigReadAllResponse)] +#[serde(rename_all = "camelCase")] +pub struct ConfigReadAllRequest {} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +#[serde(rename_all = "camelCase")] +pub struct ConfigReadAllResponse { + pub config: std::collections::HashMap, +} + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub enum PreferenceKey { @@ -448,6 +573,12 @@ pub struct DefaultsSaveRequest { pub model_id: Option, } +/// Clear Goose default provider and model configuration. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/unstable/defaults/clear", response = DefaultsReadResponse)] +#[serde(rename_all = "camelCase")] +pub struct DefaultsClearRequest {} + /// Sources that onboarding knows how to discover and import. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] @@ -783,6 +914,107 @@ pub struct ProviderConfigChangeResponse { pub refresh: RefreshProviderInventoryResponse, } +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ProviderSecretStorageDto { + #[default] + SecretStore, + ProviderCache, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ProviderSecretStatusDto { + Valid, + Expired, + #[default] + Unknown, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct ProviderSecretDto { + pub id: String, + pub provider: String, + pub provider_display_name: String, + pub name: String, + pub storage: ProviderSecretStorageDto, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub expires_at: Option, + pub status: ProviderSecretStatusDto, + pub configured: bool, + pub has_secret: bool, + pub can_delete: bool, + pub can_configure: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub configure_provider: Option, +} + +/// List provider credentials stored locally by Goose. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request( + method = "_goose/unstable/providers/secrets/list", + response = ProviderSecretsListResponse +)] +#[serde(rename_all = "camelCase")] +pub struct ProviderSecretsListRequest {} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +#[serde(rename_all = "camelCase")] +pub struct ProviderSecretsListResponse { + pub secrets: Vec, +} + +/// Delete a locally stored provider credential by id. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request( + method = "_goose/unstable/providers/secrets/delete", + response = EmptyResponse +)] +#[serde(rename_all = "camelCase")] +pub struct ProviderSecretDeleteRequest { + pub id: String, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct CanonicalModelInfoDto { + pub provider: String, + pub model: String, + pub context_limit: usize, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_output_tokens: Option, + pub reasoning: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub input_token_cost: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub output_token_cost: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cache_read_token_cost: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cache_write_token_cost: Option, + pub currency: String, +} + +/// Look up canonical (bundled-registry) model info for a provider/model pair. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request( + method = "_goose/unstable/providers/canonical-model-info", + response = CanonicalModelInfoResponse +)] +#[serde(rename_all = "camelCase")] +pub struct CanonicalModelInfoRequest { + pub provider: String, + pub model: String, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +#[serde(rename_all = "camelCase")] +pub struct CanonicalModelInfoResponse { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub model_info: Option, +} + #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct ProviderTemplateCatalogEntryDto { @@ -1620,3 +1852,32 @@ pub struct DictationModelSelectRequest { pub provider: String, pub model_id: String, } + +/// Permission level for a tool. +#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ToolPermissionLevel { + AlwaysAllow, + #[default] + AskBefore, + NeverAllow, +} + +/// A single tool permission entry. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct ToolPermissionEntry { + pub tool_name: String, + pub permission: ToolPermissionLevel, +} + +/// Set permission levels for one or more tools. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/unstable/tools/permissions/set", response = SetToolPermissionsResponse)] +#[serde(rename_all = "camelCase")] +pub struct SetToolPermissionsRequest { + pub tool_permissions: Vec, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +pub struct SetToolPermissionsResponse {} diff --git a/crates/goose-server/Cargo.toml b/crates/goose-server/Cargo.toml index ab25ab25777a..a05618a33225 100644 --- a/crates/goose-server/Cargo.toml +++ b/crates/goose-server/Cargo.toml @@ -35,7 +35,6 @@ system-keyring = ["goose/system-keyring"] portable-default = ["rustls-tls", "aws-providers", "telemetry", "otel"] rustls-tls = [ "reqwest/rustls", - "tokio-tungstenite/rustls-tls-native-roots", "axum-server/tls-rustls", "dep:rustls", "dep:aws-lc-rs", @@ -45,7 +44,6 @@ rustls-tls = [ ] native-tls = [ "reqwest/native-tls", - "tokio-tungstenite/native-tls", "axum-server/tls-openssl", "dep:openssl", "goose/native-tls", @@ -80,11 +78,8 @@ utoipa = { workspace = true, features = ["axum_extras", "chrono"] } reqwest = { workspace = true, features = ["json", "blocking", "multipart", "system-proxy"] } tokio-util = { workspace = true } serde_path_to_error = { version = "0.1.8", default-features = false } -tokio-tungstenite = { version = "0.29", default-features = false, features = ["connect"] } -url = { workspace = true } rand = { workspace = true } hex = { version = "0.4.3", default-features = false, features = ["std"] } -socket2 = { version = "0.6", default-features = false } rustls = { workspace = true, optional = true } uuid = { workspace = true } rcgen = { version = "0.14", default-features = false, features = ["aws_lc_rs", "crypto", "pem"] } diff --git a/crates/goose-server/src/lib.rs b/crates/goose-server/src/lib.rs index 2323f2877490..551a2fdbd71d 100644 --- a/crates/goose-server/src/lib.rs +++ b/crates/goose-server/src/lib.rs @@ -13,8 +13,6 @@ pub mod session_event_bus; pub mod state; #[cfg(any(feature = "rustls-tls", feature = "native-tls"))] pub mod tls; -pub mod tunnel; - // Re-export commonly used items pub use openapi::*; pub use state::*; diff --git a/crates/goose-server/src/main.rs b/crates/goose-server/src/main.rs index bbb12fe9489f..cfde78440a0b 100644 --- a/crates/goose-server/src/main.rs +++ b/crates/goose-server/src/main.rs @@ -6,8 +6,6 @@ mod openapi; mod routes; mod session_event_bus; mod state; -mod tunnel; - use std::path::PathBuf; use std::{backtrace::Backtrace, panic::PanicHookInfo}; diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index fcb242059ee3..fd26a35584f3 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -409,7 +409,6 @@ derive_utoipa!(IconTheme as IconThemeSchema); super::routes::config_management::get_provider_models, super::routes::config_management::get_provider_model_info, super::routes::config_management::get_slash_commands, - super::routes::config_management::upsert_permissions, super::routes::config_management::create_custom_provider, super::routes::config_management::get_custom_provider, super::routes::config_management::update_custom_provider, @@ -419,7 +418,6 @@ derive_utoipa!(IconTheme as IconThemeSchema); super::routes::config_management::cleanup_provider_cache, super::routes::config_management::check_provider, super::routes::config_management::set_config_provider, - super::routes::config_management::configure_provider_oauth, super::routes::config_management::get_canonical_model_info, super::routes::prompts::get_prompts, super::routes::prompts::get_prompt, @@ -469,7 +467,6 @@ derive_utoipa!(IconTheme as IconThemeSchema); super::routes::setup::start_openrouter_setup, super::routes::setup::start_tetrate_setup, super::routes::setup::start_nanogpt_setup, - super::routes::tunnel::get_tunnel_status, super::routes::telemetry::send_telemetry_event, super::routes::dictation::transcribe_dictation, super::routes::dictation::get_dictation_config, @@ -489,8 +486,6 @@ derive_utoipa!(IconTheme as IconThemeSchema); super::routes::config_management::CommandType, super::routes::config_management::ExtensionResponse, super::routes::config_management::ExtensionQuery, - super::routes::config_management::ToolPermission, - super::routes::config_management::UpsertPermissionsQuery, super::routes::config_management::UpdateCustomProviderRequest, goose::providers::catalog::ProviderCatalogEntry, goose::providers::catalog::ProviderTemplate, @@ -639,8 +634,6 @@ derive_utoipa!(IconTheme as IconThemeSchema); super::routes::agent::RestartAgentResponse, goose::agents::ExtensionLoadResult, super::routes::setup::SetupResponse, - super::tunnel::TunnelInfo, - super::tunnel::TunnelState, super::routes::telemetry::TelemetryEventRequest, goose::goose_apps::GooseApp, goose::goose_apps::WindowProps, diff --git a/crates/goose-server/src/routes/config_management.rs b/crates/goose-server/src/routes/config_management.rs index 4ad7f189fa25..0f65ef7aa873 100644 --- a/crates/goose-server/src/routes/config_management.rs +++ b/crates/goose-server/src/routes/config_management.rs @@ -7,7 +7,6 @@ use axum::{ routing::{delete, get, post}, Json, Router, }; -use chrono::{DateTime, TimeZone, Utc}; use goose::config::declarative_providers::LoadedProvider; use goose::config::paths::Paths; use goose::config::ExtensionEntry; @@ -20,20 +19,15 @@ use goose::providers::catalog::{ ProviderTemplate, }; use goose::providers::create_with_default_model; -use goose::providers::huggingface_auth; use goose::providers::providers as get_providers; use goose::{ - agents::execute_commands, agents::ExtensionConfig, config::permission::PermissionLevel, - slash_commands::recipe_slash_command, + agents::execute_commands, agents::ExtensionConfig, slash_commands::recipe_slash_command, }; use goose_providers::model::ModelConfig; use serde::{Deserialize, Serialize}; use serde_json::Value; use serde_yaml; -use std::{ - collections::{HashMap, HashSet}, - sync::Arc, -}; +use std::{collections::HashMap, sync::Arc}; use utoipa::ToSchema; #[derive(Serialize, ToSchema)] @@ -83,17 +77,6 @@ pub struct ProvidersResponse { pub providers: Vec, } -#[derive(Debug, Serialize, Deserialize, ToSchema)] -pub struct ToolPermission { - pub tool_name: String, - pub permission: PermissionLevel, -} - -#[derive(Deserialize, ToSchema)] -pub struct UpsertPermissionsQuery { - pub tool_permissions: Vec, -} - #[derive(Deserialize, ToSchema)] pub struct UpdateCustomProviderRequest { pub engine: String, @@ -146,37 +129,9 @@ pub enum ConfigValueResponse { MaskedValue(MaskedSecret), } -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum ProviderSecretStorage { - SecretStore, - ProviderCache, -} - -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum ProviderSecretStatus { - Valid, - Expired, - Unknown, -} - -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -pub struct ProviderSecret { - pub id: String, - pub provider: String, - pub provider_display_name: String, - pub name: String, - pub storage: ProviderSecretStorage, - pub expires_at: Option>, - pub status: ProviderSecretStatus, - pub configured: bool, - pub has_secret: bool, - pub can_delete: bool, - pub can_configure: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub configure_provider: Option, -} +pub use goose::providers::provider_secrets::{ + ProviderSecret, ProviderSecretStatus, ProviderSecretStorage, +}; #[derive(Debug, Serialize, ToSchema)] pub struct ProviderSecretsResponse { @@ -216,11 +171,8 @@ pub async fn upsert_config( ) -> Result, ErrorResponse> { let config = Config::global(); - // Intercept legacy keys to write structured provider config if query.key == "GOOSE_PROVIDER" { if let Some(name) = query.value.as_str() { - // Preserve the target provider's saved model rather than copying - // the current active provider's model into the new entry. let model = goose::config::get_provider_entry(config, name) .map(|e| e.model) .or_else(|| config.get_goose_model().ok()) @@ -290,360 +242,6 @@ fn mask_secret(secret: Value) -> String { format!("{}{}", visible, mask) } -const SECRET_STORE_ID_PREFIX: &str = "secret_store:"; -const PROVIDER_CACHE_ID_PREFIX: &str = "provider_cache:"; - -fn provider_secret_status(expires_at: Option>) -> ProviderSecretStatus { - match expires_at { - Some(expires_at) if expires_at <= Utc::now() => ProviderSecretStatus::Expired, - Some(_) => ProviderSecretStatus::Valid, - None => ProviderSecretStatus::Unknown, - } -} - -fn parse_expiry_value(value: &Value) -> Option> { - match value { - Value::String(value) => DateTime::parse_from_rfc3339(value) - .ok() - .map(|dt| dt.with_timezone(&Utc)), - Value::Number(value) => value - .as_i64() - .and_then(|timestamp| Utc.timestamp_opt(timestamp, 0).single()), - _ => None, - } -} - -fn find_expires_at(value: &Value) -> Option> { - match value { - Value::Object(map) => { - if map - .get("refresh_token") - .and_then(Value::as_str) - .is_some_and(|token| !token.is_empty()) - { - return None; - } - if let Some(expires_at) = map.get("expires_at").and_then(parse_expiry_value) { - return Some(expires_at); - } - if let Some(expires_at) = map.get("expires_on").and_then(parse_expiry_value) { - return Some(expires_at); - } - map.values().find_map(find_expires_at) - } - Value::Array(values) => values.iter().find_map(find_expires_at), - _ => None, - } -} - -#[derive(Clone, Copy)] -struct ProviderCacheSecretDefinition { - provider: &'static str, - name: &'static str, - path: &'static str, - is_directory: bool, -} - -const PROVIDER_CACHE_SECRET_DEFINITIONS: &[ProviderCacheSecretDefinition] = &[ - ProviderCacheSecretDefinition { - provider: "gemini_oauth", - name: "OAuth token", - path: "gemini_oauth/tokens.json", - is_directory: false, - }, - ProviderCacheSecretDefinition { - provider: "chatgpt_codex", - name: "OAuth token", - path: "chatgpt_codex/tokens.json", - is_directory: false, - }, - ProviderCacheSecretDefinition { - provider: "kimi_code", - name: "OAuth token", - path: "kimicode/token.json", - is_directory: false, - }, - ProviderCacheSecretDefinition { - provider: "github_copilot", - name: "OAuth token", - path: "githubcopilot", - is_directory: true, - }, - ProviderCacheSecretDefinition { - provider: "xai_oauth", - name: "OAuth token", - path: "xai_oauth/tokens.json", - is_directory: false, - }, - ProviderCacheSecretDefinition { - provider: "databricks", - name: "OAuth token", - path: "databricks/oauth", - is_directory: true, - }, - ProviderCacheSecretDefinition { - provider: "databricks_v2", - name: "OAuth token", - path: "databricks/oauth", - is_directory: true, - }, -]; - -fn provider_cache_definitions_for_display() -> Vec { - let mut seen_paths = HashSet::new(); - PROVIDER_CACHE_SECRET_DEFINITIONS - .iter() - .copied() - .filter(|definition| seen_paths.insert(definition.path)) - .collect() -} - -fn provider_cache_definition(provider: &str) -> Option { - PROVIDER_CACHE_SECRET_DEFINITIONS - .iter() - .copied() - .find(|definition| definition.provider == provider) -} - -fn provider_cache_providers_sharing_cache(provider: &str) -> Vec<&'static str> { - let Some(definition) = provider_cache_definition(provider) else { - return Vec::new(); - }; - - PROVIDER_CACHE_SECRET_DEFINITIONS - .iter() - .filter(|other| other.path == definition.path) - .map(|definition| definition.provider) - .collect() -} - -fn read_json_file(path: &std::path::Path) -> Option { - std::fs::read_to_string(path) - .ok() - .and_then(|contents| serde_json::from_str(&contents).ok()) -} - -fn collect_json_expiries(path: &std::path::Path, is_directory: bool) -> Vec> { - if !is_directory { - return read_json_file(path) - .and_then(|value| find_expires_at(&value)) - .into_iter() - .collect(); - } - - let mut expiries = Vec::new(); - let mut stack = vec![path.to_path_buf()]; - - while let Some(current) = stack.pop() { - let Ok(entries) = std::fs::read_dir(current) else { - continue; - }; - - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - stack.push(path); - continue; - } - if path.extension().and_then(|ext| ext.to_str()) != Some("json") { - continue; - } - if let Some(expires_at) = - read_json_file(&path).and_then(|value| find_expires_at(&value)) - { - expiries.push(expires_at); - } - } - } - - expiries -} - -fn provider_cache_exists(path: &std::path::Path, is_directory: bool) -> bool { - if !is_directory { - return path.is_file(); - } - - let Ok(entries) = std::fs::read_dir(path) else { - return false; - }; - - entries.flatten().any(|entry| { - let path = entry.path(); - path.is_file() || provider_cache_exists(&path, true) - }) -} - -fn provider_cache_expiry(definition: ProviderCacheSecretDefinition) -> Option> { - let path = Paths::in_config_dir(definition.path); - let expiries = collect_json_expiries(&path, definition.is_directory); - expiries.into_iter().min() -} - -fn build_provider_cache_secret( - definition: ProviderCacheSecretDefinition, - display_names: &HashMap, -) -> Option { - let path = Paths::in_config_dir(definition.path); - if !provider_cache_exists(&path, definition.is_directory) { - return None; - } - - let expires_at = provider_cache_expiry(definition); - Some(ProviderSecret { - id: format!("{}{}", PROVIDER_CACHE_ID_PREFIX, definition.provider), - provider: definition.provider.to_string(), - provider_display_name: display_names - .get(definition.provider) - .cloned() - .unwrap_or_else(|| definition.provider.to_string()), - name: definition.name.to_string(), - storage: ProviderSecretStorage::ProviderCache, - expires_at, - status: provider_secret_status(expires_at), - configured: true, - has_secret: true, - can_delete: true, - can_configure: false, - configure_provider: None, - }) -} - -fn build_huggingface_oauth_secret( - token: Option, -) -> ProviderSecret { - let expires_at = token.as_ref().and_then(|token| token.expires_at); - let has_secret = token.is_some(); - - ProviderSecret { - id: format!( - "{}{}", - PROVIDER_CACHE_ID_PREFIX, - huggingface_auth::HUGGINGFACE_PROVIDER_NAME - ), - provider: huggingface_auth::HUGGINGFACE_PROVIDER_NAME.to_string(), - provider_display_name: huggingface_auth::HUGGINGFACE_DISPLAY_NAME.to_string(), - name: huggingface_auth::HUGGINGFACE_OAUTH_TOKEN_NAME.to_string(), - storage: ProviderSecretStorage::ProviderCache, - expires_at, - status: provider_secret_status(expires_at), - configured: has_secret, - has_secret, - can_delete: has_secret, - can_configure: true, - configure_provider: Some(huggingface_auth::HUGGINGFACE_PROVIDER_NAME.to_string()), - } -} - -fn build_secret_store_secrets( - stored_secrets: &HashMap, - providers: &[(ProviderMetadata, ProviderType)], -) -> Vec { - let mut secrets = Vec::new(); - - for (metadata, _) in providers { - for config_key in metadata.config_keys.iter().filter(|key| key.secret) { - if !stored_secrets.contains_key(&config_key.name) { - continue; - } - secrets.push(ProviderSecret { - id: format!( - "{}{}:{}", - SECRET_STORE_ID_PREFIX, metadata.name, config_key.name - ), - provider: metadata.name.clone(), - provider_display_name: metadata.display_name.clone(), - name: config_key.name.clone(), - storage: ProviderSecretStorage::SecretStore, - expires_at: None, - status: ProviderSecretStatus::Unknown, - configured: true, - has_secret: true, - can_delete: true, - can_configure: false, - configure_provider: None, - }); - } - } - - secrets -} - -fn is_known_provider_secret( - providers: &[(ProviderMetadata, ProviderType)], - provider: &str, - key: &str, -) -> bool { - providers - .iter() - .filter(|(metadata, _)| metadata.name == provider) - .flat_map(|(metadata, _)| metadata.config_keys.iter()) - .any(|config_key| config_key.secret && config_key.name == key) -} - -fn unconfigure_provider(config: &Config, provider_name: &str) -> Result<(), ConfigError> { - if let Some(mut entry) = goose::config::get_provider_entry(config, provider_name) { - entry.configured = false; - goose::config::set_provider_entry(config, provider_name, &entry)?; - } - - let configured_marker = format!("{}_configured", provider_name); - config.delete(&configured_marker)?; - Ok(()) -} - -fn mark_provider_configured(config: &Config, provider_name: &str) -> Result<(), ConfigError> { - if let Some(mut entry) = goose::config::get_provider_entry(config, provider_name) { - entry.configured = true; - goose::config::set_provider_entry(config, provider_name, &entry)?; - } else { - let model = if goose::config::get_active_provider(config).as_deref() == Some(provider_name) - { - config.get_goose_model().unwrap_or_default() - } else { - String::new() - }; - goose::config::set_provider_entry( - config, - provider_name, - &goose::config::ProviderEntry { - enabled: true, - model, - configured: true, - }, - )?; - } - - Ok(()) -} - -fn parse_secret_store_id(id: &str) -> Option<(&str, &str)> { - let rest = id.strip_prefix(SECRET_STORE_ID_PREFIX)?; - let (provider, key) = rest.split_once(':')?; - Some((provider, key)) -} - -fn parse_provider_cache_id(id: &str) -> Option<&str> { - id.strip_prefix(PROVIDER_CACHE_ID_PREFIX) -} - -fn is_valid_provider_name(provider_name: &str) -> bool { - !provider_name.is_empty() - && provider_name - .chars() - .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') -} - -fn should_unconfigure_after_secret_delete( - provider: &str, - key: &str, - has_usable_huggingface_oauth_token: impl FnOnce() -> bool, -) -> bool { - provider == huggingface_auth::HUGGINGFACE_PROVIDER_NAME - && key == huggingface_auth::HUGGINGFACE_TOKEN_SECRET_KEY - && !has_usable_huggingface_oauth_token() -} - #[utoipa::path( get, path = "/config/provider-secrets", @@ -653,40 +251,7 @@ fn should_unconfigure_after_secret_delete( ) )] pub async fn list_provider_secrets() -> Result, ErrorResponse> { - let config = Config::global(); - let stored_secrets = config.all_secrets()?; - let providers = get_providers().await; - let display_names: HashMap = providers - .iter() - .map(|(metadata, _)| (metadata.name.clone(), metadata.display_name.clone())) - .collect(); - - let mut secrets = build_secret_store_secrets(&stored_secrets, &providers); - - for definition in provider_cache_definitions_for_display() { - if let Some(secret) = build_provider_cache_secret(definition, &display_names) { - if !secrets.iter().any(|existing| existing.id == secret.id) { - secrets.push(secret); - } - } - } - - let huggingface_secret = build_huggingface_oauth_secret(huggingface_auth::load_oauth_token()); - if let Some(existing) = secrets - .iter_mut() - .find(|existing| existing.id == huggingface_secret.id) - { - *existing = huggingface_secret; - } else { - secrets.push(huggingface_secret); - } - - secrets.sort_by(|a, b| { - a.provider_display_name - .cmp(&b.provider_display_name) - .then_with(|| a.name.cmp(&b.name)) - }); - + let secrets = goose::providers::provider_secrets::list_provider_secrets().await?; Ok(Json(ProviderSecretsResponse { secrets })) } @@ -703,52 +268,17 @@ pub async fn list_provider_secrets() -> Result, Er ) )] pub async fn delete_provider_secret(Path(id): Path) -> Result, ErrorResponse> { - let config = Config::global(); - - if let Some((provider, key)) = parse_secret_store_id(&id) { - let providers = get_providers().await; - if !is_known_provider_secret(&providers, provider, key) { - return Err(ErrorResponse::bad_request(format!( - "Invalid provider secret id: '{}'", - id - ))); - } + use goose::providers::provider_secrets::DeleteProviderSecretError; - config.delete_secret(key)?; - if should_unconfigure_after_secret_delete(provider, key, || { - huggingface_auth::has_configured_token().unwrap_or(false) - }) { - unconfigure_provider(config, provider)?; - } - return Ok(Json(format!("Deleted provider secret {}", id))); + match goose::providers::provider_secrets::delete_provider_secret(&id).await { + Ok(()) => Ok(Json(format!("Deleted provider secret {}", id))), + Err(DeleteProviderSecretError::InvalidId(id)) => Err(ErrorResponse::bad_request(format!( + "Invalid provider secret id: '{}'", + id + ))), + Err(DeleteProviderSecretError::Config(e)) => Err(e.into()), + Err(DeleteProviderSecretError::Other(e)) => Err(ErrorResponse::internal(e.to_string())), } - - if let Some(provider) = parse_provider_cache_id(&id) { - if provider == huggingface_auth::HUGGINGFACE_PROVIDER_NAME { - huggingface_auth::clear_oauth_token()?; - unconfigure_provider(config, provider)?; - return Ok(Json(format!("Deleted provider secret {}", id))); - } - - let cache_definition = provider_cache_definition(provider); - - if !is_valid_provider_name(provider) || cache_definition.is_none() { - return Err(ErrorResponse::bad_request(format!( - "Invalid provider name: '{}'", - provider - ))); - } - goose::providers::cleanup_provider(provider).await?; - for shared_provider in provider_cache_providers_sharing_cache(provider) { - unconfigure_provider(config, shared_provider)?; - } - return Ok(Json(format!("Deleted provider secret {}", id))); - } - - Err(ErrorResponse::bad_request(format!( - "Invalid provider secret id: '{}'", - id - ))) } #[utoipa::path( @@ -765,7 +295,6 @@ pub async fn read_config( ) -> Result, ErrorResponse> { let config = Config::global(); - // Intercept legacy keys to return structured provider config if query.key == "GOOSE_PROVIDER" || query.key == "active_provider" { if let Ok(val) = config.get_goose_provider() { return Ok(Json(ConfigValueResponse::Value(Value::String(val)))); @@ -1139,30 +668,6 @@ pub async fn get_canonical_model_info( }) } -#[utoipa::path( - post, - path = "/config/permissions", - request_body = UpsertPermissionsQuery, - responses( - (status = 200, description = "Permission update completed", body = String), - (status = 400, description = "Invalid request"), - ) -)] -pub async fn upsert_permissions( - Json(query): Json, -) -> Result, ErrorResponse> { - let permission_manager = goose::config::PermissionManager::instance(); - - for tool_permission in &query.tool_permissions { - permission_manager.update_user_permission( - &tool_permission.tool_name, - tool_permission.permission.clone(), - ); - } - - Ok(Json("Permissions updated successfully".to_string())) -} - #[utoipa::path( get, path = "/config/validate", @@ -1324,7 +829,6 @@ pub async fn update_custom_provider( pub async fn check_provider( Json(CheckProviderRequest { provider }): Json, ) -> Result<(), ErrorResponse> { - // Provider check does not use extensions. create_with_default_model(&provider, Vec::new()) .await .map_err(|err| { @@ -1341,7 +845,6 @@ pub async fn check_provider( pub async fn set_config_provider( Json(SetProviderRequest { provider, model }): Json, ) -> Result<(), ErrorResponse> { - // Provider validation does not use extensions. create_with_default_model(&provider, Vec::new()) .await .and_then(|_| { @@ -1406,60 +909,6 @@ pub async fn get_provider_catalog_template( Ok(Json(template)) } -#[utoipa::path( - post, - path = "/config/providers/{name}/oauth", - params( - ("name" = String, Path, description = "Provider name") - ), - responses( - (status = 200, description = "OAuth configuration completed"), - (status = 400, description = "OAuth configuration failed") - ) -)] -pub async fn configure_provider_oauth( - Path(provider_name): Path, -) -> Result, ErrorResponse> { - use goose::providers::create; - - if !is_valid_provider_name(&provider_name) { - return Err(ErrorResponse::bad_request(format!( - "Invalid provider name: '{}'", - provider_name - ))); - } - - if provider_name == huggingface_auth::HUGGINGFACE_PROVIDER_NAME { - huggingface_auth::configure_oauth().await.map_err(|e| { - ErrorResponse::bad_request(format!( - "OAuth configuration failed for provider '{}': {}", - provider_name, e - )) - })?; - mark_provider_configured(goose::config::Config::global(), &provider_name)?; - return Ok(Json("OAuth configuration completed".to_string())); - } - - // OAuth configuration does not use extensions. - let provider = create(&provider_name, Vec::new()).await.map_err(|e| { - ErrorResponse::bad_request(format!( - "Failed to create provider '{}': {}", - provider_name, e - )) - })?; - - provider.configure_oauth().await.map_err(|e| { - ErrorResponse::bad_request(format!( - "OAuth configuration failed for provider '{}': {}", - provider_name, e - )) - })?; - - mark_provider_configured(goose::config::Config::global(), &provider_name)?; - - Ok(Json("OAuth configuration completed".to_string())) -} - pub fn routes(state: Arc) -> Router { Router::new() .route("/config", get(read_all_config)) @@ -1495,7 +944,6 @@ pub fn routes(state: Arc) -> Router { post(get_canonical_model_info), ) .route("/config/validate", get(validate_config)) - .route("/config/permissions", post(upsert_permissions)) .route("/config/custom-providers", post(create_custom_provider)) .route( "/config/custom-providers/{id}", @@ -1505,289 +953,5 @@ pub fn routes(state: Arc) -> Router { .route("/config/custom-providers/{id}", get(get_custom_provider)) .route("/config/check_provider", post(check_provider)) .route("/config/set_provider", post(set_config_provider)) - .route( - "/config/providers/{name}/oauth", - post(configure_provider_oauth), - ) .with_state(state) } - -#[cfg(test)] -mod tests { - use super::*; - use goose::config::ProviderEntry; - use goose::providers::base::ConfigKey; - use serde_json::json; - - fn new_test_config() -> Config { - let unique = format!( - "goose-server-config-test-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_nanos() - ); - let config_path = std::env::temp_dir().join(format!("{unique}-config.yaml")); - let secrets_path = std::env::temp_dir().join(format!("{unique}-secrets.yaml")); - Config::new_with_file_secrets(config_path, secrets_path).unwrap() - } - - #[test] - fn secret_store_listing_only_includes_provider_secret_keys() { - let metadata = ProviderMetadata::new( - "openai", - "OpenAI", - "OpenAI provider", - "gpt-4o", - vec![], - "https://example.com", - vec![ - ConfigKey::new("OPENAI_API_KEY", true, true, None, true), - ConfigKey::new("OPENAI_HOST", false, false, None, false), - ], - ); - let providers = vec![(metadata, ProviderType::Builtin)]; - let stored_secrets = HashMap::from([ - ( - "OPENAI_API_KEY".to_string(), - Value::String("secret-value".to_string()), - ), - ( - "UNRELATED_SECRET".to_string(), - Value::String("other-secret".to_string()), - ), - ( - "OPENAI_HOST".to_string(), - Value::String("https://api.openai.com".to_string()), - ), - ]); - - let secrets = build_secret_store_secrets(&stored_secrets, &providers); - - assert_eq!(secrets.len(), 1); - assert_eq!(secrets[0].id, "secret_store:openai:OPENAI_API_KEY"); - assert_eq!(secrets[0].provider_display_name, "OpenAI"); - assert_eq!(secrets[0].name, "OPENAI_API_KEY"); - assert_eq!(secrets[0].storage, ProviderSecretStorage::SecretStore); - assert_eq!(secrets[0].status, ProviderSecretStatus::Unknown); - } - - #[test] - fn provider_secret_delete_validation_requires_provider_secret_key() { - let metadata = ProviderMetadata::new( - "openai", - "OpenAI", - "OpenAI provider", - "gpt-4o", - vec![], - "https://example.com", - vec![ - ConfigKey::new("OPENAI_API_KEY", true, true, None, true), - ConfigKey::new("OPENAI_HOST", false, false, None, false), - ], - ); - let providers = vec![(metadata, ProviderType::Builtin)]; - - assert!(is_known_provider_secret( - &providers, - "openai", - "OPENAI_API_KEY" - )); - assert!(!is_known_provider_secret( - &providers, - "openai", - "OPENAI_HOST" - )); - assert!(!is_known_provider_secret( - &providers, - "openai", - "UNRELATED_SECRET" - )); - assert!(!is_known_provider_secret( - &providers, - "anthropic", - "OPENAI_API_KEY" - )); - } - - #[test] - fn expiry_extraction_handles_nested_rfc3339_values() { - let expires_at = Utc::now() + chrono::Duration::hours(1); - let value = json!({ - "project_id": "project", - "token": { - "access_token": "secret", - "expires_at": expires_at.to_rfc3339(), - } - }); - - let parsed = find_expires_at(&value).expect("expected expiry"); - - assert_eq!(parsed.timestamp(), expires_at.timestamp()); - assert_eq!( - provider_secret_status(Some(parsed)), - ProviderSecretStatus::Valid - ); - } - - #[test] - fn expiry_extraction_ignores_refreshable_access_tokens() { - let expires_at = Utc::now() - chrono::Duration::hours(1); - let value = json!({ - "access_token": "access", - "refresh_token": "refresh", - "expires_at": expires_at.to_rfc3339(), - }); - - assert_eq!(find_expires_at(&value), None); - } - - #[test] - fn expiry_extraction_handles_expired_unix_timestamps() { - let value = json!({ - "info": { - "expires_at": 1 - } - }); - - let parsed = find_expires_at(&value).expect("expected expiry"); - - assert_eq!(parsed.timestamp(), 1); - assert_eq!( - provider_secret_status(Some(parsed)), - ProviderSecretStatus::Expired - ); - } - - #[test] - fn provider_secret_ids_parse_expected_prefixes() { - assert_eq!( - parse_secret_store_id("secret_store:openai:OPENAI_API_KEY"), - Some(("openai", "OPENAI_API_KEY")) - ); - assert_eq!( - parse_provider_cache_id("provider_cache:gemini_oauth"), - Some("gemini_oauth") - ); - assert_eq!(parse_secret_store_id("provider_cache:openai"), None); - assert_eq!(parse_provider_cache_id("secret_store:openai:key"), None); - } - - #[test] - fn shared_databricks_cache_is_displayed_once() { - let databricks_definitions: Vec<_> = provider_cache_definitions_for_display() - .into_iter() - .filter(|definition| definition.path == "databricks/oauth") - .collect(); - - assert_eq!(databricks_definitions.len(), 1); - assert_eq!(databricks_definitions[0].provider, "databricks"); - } - - #[test] - fn shared_databricks_cache_unconfigures_both_providers() { - assert_eq!( - provider_cache_providers_sharing_cache("databricks"), - vec!["databricks", "databricks_v2"] - ); - assert_eq!( - provider_cache_providers_sharing_cache("databricks_v2"), - vec!["databricks", "databricks_v2"] - ); - } - - #[test] - fn unconfigure_provider_clears_structured_entry() { - let config = new_test_config(); - goose::config::set_provider_entry( - &config, - "huggingface", - &ProviderEntry { - enabled: true, - model: "Qwen/Qwen3-Coder-480B-A35B-Instruct".to_string(), - configured: true, - }, - ) - .unwrap(); - - unconfigure_provider(&config, "huggingface").unwrap(); - - let entry = goose::config::get_provider_entry(&config, "huggingface").unwrap(); - assert!(entry.enabled); - assert_eq!(entry.model, "Qwen/Qwen3-Coder-480B-A35B-Instruct"); - assert!(!entry.configured); - } - - #[test] - fn unconfigure_provider_deletes_legacy_configured_marker() { - let config = new_test_config(); - config.set_param("huggingface_configured", true).unwrap(); - - unconfigure_provider(&config, "huggingface").unwrap(); - - assert!(config.get_param::("huggingface_configured").is_err()); - } - - #[test] - fn deleting_huggingface_token_unconfigures_without_oauth() { - assert!(should_unconfigure_after_secret_delete( - "huggingface", - "HF_TOKEN", - || false - )); - } - - #[test] - fn deleting_huggingface_token_keeps_configured_with_oauth() { - assert!(!should_unconfigure_after_secret_delete( - "huggingface", - "HF_TOKEN", - || true - )); - } - - #[test] - fn deleting_other_provider_secret_does_not_unconfigure_huggingface() { - assert!(!should_unconfigure_after_secret_delete( - "openai", - "OPENAI_API_KEY", - || false - )); - } - - #[test] - fn huggingface_oauth_secret_is_permanent_without_token() { - let secret = build_huggingface_oauth_secret(None); - - assert_eq!(secret.id, "provider_cache:huggingface"); - assert_eq!(secret.provider_display_name, "Hugging Face"); - assert_eq!(secret.name, "OAuth token"); - assert_eq!(secret.storage, ProviderSecretStorage::ProviderCache); - assert_eq!(secret.status, ProviderSecretStatus::Unknown); - assert!(!secret.configured); - assert!(!secret.has_secret); - assert!(!secret.can_delete); - assert!(secret.can_configure); - assert_eq!(secret.configure_provider.as_deref(), Some("huggingface")); - } - - #[test] - fn huggingface_oauth_secret_reports_cached_token_metadata() { - let expires_at = Utc::now() + chrono::Duration::hours(1); - let secret = build_huggingface_oauth_secret(Some(huggingface_auth::HuggingFaceTokenData { - access_token: "hidden".to_string(), - refresh_token: None, - expires_at: Some(expires_at), - })); - - assert_eq!( - secret.expires_at.map(|value| value.timestamp()), - Some(expires_at.timestamp()) - ); - assert_eq!(secret.status, ProviderSecretStatus::Valid); - assert!(secret.configured); - assert!(secret.has_secret); - assert!(secret.can_delete); - } -} diff --git a/crates/goose-server/src/routes/dictation.rs b/crates/goose-server/src/routes/dictation.rs index e7c8111451c5..78602223e4a6 100644 --- a/crates/goose-server/src/routes/dictation.rs +++ b/crates/goose-server/src/routes/dictation.rs @@ -85,7 +85,7 @@ fn validate_audio(audio: &str, mime_type: &str) -> Result<(Vec, &'static str return Err(ErrorResponse { message: format!("Unsupported audio format: {}", mime_type), status: StatusCode::UNSUPPORTED_MEDIA_TYPE, - }) + }); } }; diff --git a/crates/goose-server/src/routes/mod.rs b/crates/goose-server/src/routes/mod.rs index 0c2bae87789d..8ed585f797b6 100644 --- a/crates/goose-server/src/routes/mod.rs +++ b/crates/goose-server/src/routes/mod.rs @@ -18,7 +18,6 @@ pub mod session_events; pub mod setup; pub mod status; pub mod telemetry; -pub mod tunnel; pub mod utils; use std::sync::Arc; @@ -39,7 +38,6 @@ pub fn configure(state: Arc, secret_key: String) -> Rout .merge(schedule::routes(state.clone())) .merge(setup::routes(state.clone())) .merge(telemetry::routes(state.clone())) - .merge(tunnel::routes(state.clone())) .merge(mcp_ui_proxy::routes(secret_key.clone())) .merge(mcp_app_proxy::routes(secret_key)) .merge(session_events::routes(state.clone())) diff --git a/crates/goose-server/src/routes/tunnel.rs b/crates/goose-server/src/routes/tunnel.rs deleted file mode 100644 index 023c3aba46e0..000000000000 --- a/crates/goose-server/src/routes/tunnel.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::state::AppState; -use axum::{ - extract::State, - http::StatusCode, - response::{IntoResponse, Response}, - routing::get, - Json, Router, -}; -use std::sync::Arc; - -/// Get tunnel info -#[utoipa::path( - get, - path = "/tunnel/status", - responses( - (status = 200, description = "Tunnel info", body = TunnelInfo) - ) -)] -pub async fn get_tunnel_status(State(state): State>) -> Response { - let info = state.tunnel_manager.get_info().await; - (StatusCode::OK, Json(info)).into_response() -} - -pub fn routes(state: Arc) -> Router { - Router::new() - .route("/tunnel/status", get(get_tunnel_status)) - .with_state(state) -} diff --git a/crates/goose-server/src/state.rs b/crates/goose-server/src/state.rs index 8fd5934037cb..afd2c4c0f279 100644 --- a/crates/goose-server/src/state.rs +++ b/crates/goose-server/src/state.rs @@ -12,7 +12,6 @@ use tokio::sync::Mutex; use tokio::task::JoinHandle; use crate::session_event_bus::SessionEventBus; -use crate::tunnel::TunnelManager; use goose::agents::ExtensionLoadResult; #[cfg(feature = "local-inference")] use goose::providers::local_inference::InferenceRuntime; @@ -25,7 +24,6 @@ pub struct AppState { pub(crate) agent_manager: Arc, pub recipe_file_hash_map: Arc>>, recipe_session_tracker: Arc>>, - pub tunnel_manager: Arc, pub extension_loading_tasks: ExtensionLoadingTasks, #[cfg(feature = "local-inference")] inference_runtime: Arc>>, @@ -33,17 +31,14 @@ pub struct AppState { } impl AppState { - pub async fn new(tls: bool) -> anyhow::Result> { + pub async fn new(_tls: bool) -> anyhow::Result> { register_builtin_extensions(goose_mcp::BUILTIN_EXTENSIONS.clone()); let agent_manager = AgentManager::instance().await?; - let tunnel_manager = Arc::new(TunnelManager::new(tls)); - Ok(Arc::new(Self { agent_manager, recipe_file_hash_map: Arc::new(Mutex::new(HashMap::new())), recipe_session_tracker: Arc::new(Mutex::new(HashSet::new())), - tunnel_manager, extension_loading_tasks: Arc::new(Mutex::new(HashMap::new())), #[cfg(feature = "local-inference")] inference_runtime: Arc::new(OnceLock::new()), diff --git a/crates/goose-server/src/tunnel/lapstone.rs b/crates/goose-server/src/tunnel/lapstone.rs deleted file mode 100644 index 9b55afbf306b..000000000000 --- a/crates/goose-server/src/tunnel/lapstone.rs +++ /dev/null @@ -1,635 +0,0 @@ -use super::TunnelInfo; -use anyhow::{Context, Result}; -use futures::{SinkExt, StreamExt}; -use reqwest; -use serde::{Deserialize, Serialize}; -use socket2::{SockRef, TcpKeepalive}; -use std::collections::HashMap; -use std::sync::Arc; -use std::time::{Duration, Instant}; -use tokio::sync::{mpsc, RwLock}; -use tokio::task::JoinHandle; -use tokio_tungstenite::{connect_async, tungstenite::Message}; -use tracing::{error, info, warn}; -use url::Url; - -/// Shared state for proxying tunnel requests to the local goosed server. -#[derive(Clone)] -struct ProxyContext { - port: u16, - tunnel_secret: String, - server_secret: String, - http_client: reqwest::Client, -} - -/// Constant-time comparison using hash to prevent timing attacks -fn secure_compare(a: &str, b: &str) -> bool { - use std::collections::hash_map::DefaultHasher; - use std::hash::{Hash, Hasher}; - - let mut hasher_a = DefaultHasher::new(); - a.hash(&mut hasher_a); - let hash_a = hasher_a.finish(); - - let mut hasher_b = DefaultHasher::new(); - b.hash(&mut hasher_b); - let hash_b = hasher_b.finish(); - - hash_a == hash_b -} - -const WORKER_URL: &str = "https://cloudflare-tunnel-proxy.michael-neale.workers.dev"; -const IDLE_TIMEOUT_SECS: u64 = 300; -const CONNECTION_TIMEOUT_SECS: u64 = 30; -const MAX_WS_SIZE: usize = 900_000; - -fn get_worker_url() -> String { - std::env::var("GOOSE_TUNNEL_WORKER_URL") - .ok() - .unwrap_or_else(|| WORKER_URL.to_string()) -} - -type WebSocketSender = Arc< - RwLock< - Option< - futures::stream::SplitSink< - tokio_tungstenite::WebSocketStream< - tokio_tungstenite::MaybeTlsStream, - >, - Message, - >, - >, - >, ->; - -#[derive(Debug, Serialize, Deserialize)] -struct TunnelMessage { - #[serde(rename = "requestId")] - request_id: String, - method: String, - path: String, - #[serde(skip_serializing_if = "Option::is_none")] - headers: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - body: Option, -} - -#[derive(Debug, Serialize)] -struct TunnelResponse { - #[serde(rename = "requestId")] - request_id: String, - status: u16, - #[serde(skip_serializing_if = "Option::is_none")] - headers: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - body: Option, - #[serde(skip_serializing_if = "Option::is_none")] - error: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[serde(rename = "chunkIndex")] - chunk_index: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[serde(rename = "totalChunks")] - total_chunks: Option, - #[serde(rename = "isChunked")] - is_chunked: bool, - #[serde(rename = "isStreaming")] - is_streaming: bool, - #[serde(rename = "isFirstChunk")] - is_first_chunk: bool, - #[serde(rename = "isLastChunk")] - is_last_chunk: bool, -} - -fn validate_and_build_request( - client: &reqwest::Client, - url: &str, - message: &TunnelMessage, - tunnel_secret: &str, - server_secret: &str, -) -> Result { - let incoming_secret = message - .headers - .as_ref() - .and_then(|h| { - h.iter() - .find(|(k, _)| k.eq_ignore_ascii_case("x-secret-key")) - .map(|(_, v)| v) - }) - .ok_or_else(|| anyhow::anyhow!("Missing tunnel secret header"))?; - - if !secure_compare(incoming_secret, tunnel_secret) { - anyhow::bail!("Invalid tunnel secret"); - } - - let mut request_builder = match message.method.as_str() { - "GET" => client.get(url), - "POST" => client.post(url), - "PUT" => client.put(url), - "DELETE" => client.delete(url), - "PATCH" => client.patch(url), - _ => client.get(url), - }; - - if let Some(headers) = &message.headers { - for (key, value) in headers { - if key.eq_ignore_ascii_case("x-secret-key") { - continue; - } - request_builder = request_builder.header(key, value); - } - } - - request_builder = request_builder.header("X-Secret-Key", server_secret); - - if let Some(body) = &message.body { - if message.method != "GET" && message.method != "HEAD" { - request_builder = request_builder.body(body.clone()); - } - } - - Ok(request_builder) -} - -async fn handle_streaming_response( - response: reqwest::Response, - status: u16, - headers_map: HashMap, - request_id: String, - message_path: String, - ws_tx: WebSocketSender, -) -> Result<()> { - info!("← {} {} [{}] (streaming)", status, message_path, request_id); - - let mut stream = response.bytes_stream(); - let mut chunk_index = 0; - let mut is_first_chunk = true; - - while let Some(chunk_result) = stream.next().await { - match chunk_result { - Ok(chunk) => { - let chunk_str = String::from_utf8_lossy(&chunk).to_string(); - let tunnel_response = TunnelResponse { - request_id: request_id.clone(), - status, - headers: if is_first_chunk { - Some(headers_map.clone()) - } else { - None - }, - body: Some(chunk_str), - error: None, - chunk_index: Some(chunk_index), - total_chunks: None, - is_chunked: false, - is_streaming: true, - is_first_chunk, - is_last_chunk: false, - }; - send_response(ws_tx.clone(), tunnel_response).await?; - chunk_index += 1; - is_first_chunk = false; - } - Err(e) => { - error!("Error reading stream chunk: {}", e); - break; - } - } - } - - let tunnel_response = TunnelResponse { - request_id: request_id.clone(), - status, - headers: None, - body: Some(String::new()), - error: None, - chunk_index: Some(chunk_index), - total_chunks: None, - is_chunked: false, - is_streaming: true, - is_first_chunk: false, - is_last_chunk: true, - }; - send_response(ws_tx, tunnel_response).await?; - info!( - "← {} {} [{}] (complete, {} chunks)", - status, message_path, request_id, chunk_index - ); - Ok(()) -} - -async fn handle_chunked_response( - body: String, - status: u16, - headers_map: HashMap, - request_id: String, - message_path: String, - ws_tx: WebSocketSender, -) -> Result<()> { - let total_chunks = body.len().div_ceil(MAX_WS_SIZE); - info!( - "← {} {} [{}] ({} bytes, {} chunks)", - status, - message_path, - request_id, - body.len(), - total_chunks - ); - - for (i, chunk) in body.as_bytes().chunks(MAX_WS_SIZE).enumerate() { - let chunk_str = String::from_utf8_lossy(chunk).to_string(); - let tunnel_response = TunnelResponse { - request_id: request_id.clone(), - status, - headers: if i == 0 { - Some(headers_map.clone()) - } else { - None - }, - body: Some(chunk_str), - error: None, - chunk_index: Some(i), - total_chunks: Some(total_chunks), - is_chunked: true, - is_streaming: false, - is_first_chunk: false, - is_last_chunk: false, - }; - send_response(ws_tx.clone(), tunnel_response).await?; - } - Ok(()) -} - -async fn handle_request( - message: TunnelMessage, - ctx: ProxyContext, - ws_tx: WebSocketSender, - scheme: &str, -) -> Result<()> { - let request_id = message.request_id.clone(); - let client = &ctx.http_client; - - let url = format!("{}://127.0.0.1:{}{}", scheme, ctx.port, message.path); - - let request_builder = match validate_and_build_request( - client, - &url, - &message, - &ctx.tunnel_secret, - &ctx.server_secret, - ) { - Ok(builder) => builder, - Err(e) => { - error!("✗ Authentication error [{}]: {}", request_id, e); - let error_response = TunnelResponse { - request_id, - status: 401, - headers: None, - body: None, - error: Some(e.to_string()), - chunk_index: None, - total_chunks: None, - is_chunked: false, - is_streaming: false, - is_first_chunk: false, - is_last_chunk: false, - }; - send_response(ws_tx, error_response).await?; - return Ok(()); - } - }; - - let response = match request_builder.send().await { - Ok(resp) => resp, - Err(e) => { - error!("✗ Request error [{}]: {}", request_id, e); - let error_response = TunnelResponse { - request_id, - status: 500, - headers: None, - body: None, - error: Some(e.to_string()), - chunk_index: None, - total_chunks: None, - is_chunked: false, - is_streaming: false, - is_first_chunk: false, - is_last_chunk: false, - }; - send_response(ws_tx, error_response).await?; - return Ok(()); - } - }; - - let status = response.status().as_u16(); - // Normalize header names to lowercase per RFC 7230 (HTTP headers are case-insensitive) - let headers_map: HashMap = response - .headers() - .iter() - .map(|(k, v)| { - ( - k.as_str().to_lowercase(), - v.to_str().unwrap_or("").to_string(), - ) - }) - .collect(); - - let is_streaming = headers_map - .get("content-type") - .map(|ct| ct.contains("text/event-stream")) - .unwrap_or(false); - - if is_streaming { - handle_streaming_response( - response, - status, - headers_map, - request_id, - message.path, - ws_tx, - ) - .await?; - } else { - let body = response.text().await.unwrap_or_default(); - - if body.len() > MAX_WS_SIZE { - handle_chunked_response(body, status, headers_map, request_id, message.path, ws_tx) - .await?; - } else { - let tunnel_response = TunnelResponse { - request_id: request_id.clone(), - status, - headers: Some(headers_map), - body: Some(body), - error: None, - chunk_index: None, - total_chunks: None, - is_chunked: false, - is_streaming: false, - is_first_chunk: false, - is_last_chunk: false, - }; - send_response(ws_tx, tunnel_response).await?; - } - } - - Ok(()) -} - -async fn send_response(ws_tx: WebSocketSender, response: TunnelResponse) -> Result<()> { - let json = serde_json::to_string(&response)?; - if let Some(tx) = ws_tx.write().await.as_mut() { - tx.send(Message::Text(json.into())) - .await - .context("Failed to send response")?; - } - Ok(()) -} - -fn configure_tcp_keepalive( - stream: &tokio_tungstenite::WebSocketStream< - tokio_tungstenite::MaybeTlsStream, - >, -) { - let tcp_stream = stream.get_ref().get_ref(); - let socket_ref = SockRef::from(tcp_stream); - - let keepalive = TcpKeepalive::new() - .with_time(Duration::from_secs(30)) - .with_interval(Duration::from_secs(30)); - - if let Err(e) = socket_ref.set_tcp_keepalive(&keepalive) { - warn!("Failed to set TCP keep-alive: {}", e); - } else { - info!("✓ TCP keep-alive enabled (30s interval)"); - } -} - -async fn handle_websocket_messages( - mut read: futures::stream::SplitStream< - tokio_tungstenite::WebSocketStream< - tokio_tungstenite::MaybeTlsStream, - >, - >, - ws_tx: WebSocketSender, - ctx: ProxyContext, - last_activity: Arc>, - active_tasks: Arc>>>, - scheme: String, -) { - while let Some(msg) = read.next().await { - match msg { - Ok(Message::Text(text)) => { - *last_activity.write().await = Instant::now(); - - match serde_json::from_str::(&text) { - Ok(tunnel_msg) => { - let ws_tx_clone = ws_tx.clone(); - let ctx_clone = ctx.clone(); - let scheme_clone = scheme.clone(); - let task = tokio::spawn(async move { - if let Err(e) = - handle_request(tunnel_msg, ctx_clone, ws_tx_clone, &scheme_clone) - .await - { - error!("Error handling request: {}", e); - } - }); - { - let mut tasks = active_tasks.write().await; - tasks.retain(|t| !t.is_finished()); - tasks.push(task); - } - } - Err(e) => { - error!("Error parsing tunnel message: {}", e); - } - } - } - Ok(Message::Close(_)) => { - info!("✗ Connection closed by server"); - break; - } - Ok(Message::Ping(_)) | Ok(Message::Pong(_)) => { - *last_activity.write().await = Instant::now(); - } - Err(e) => { - error!("✗ WebSocket error: {}", e); - break; - } - _ => {} - } - } -} - -async fn cleanup_connection( - ws_tx: WebSocketSender, - active_tasks: Arc>>>, -) { - if let Some(mut tx) = ws_tx.write().await.take() { - let _ = tx.close().await; - } - - let tasks = active_tasks.write().await.drain(..).collect::>(); - info!("Aborting {} active request tasks", tasks.len()); - for task in tasks { - task.abort(); - } -} - -async fn run_single_connection( - port: u16, - agent_id: String, - tunnel_secret: String, - server_secret: String, - scheme: String, - restart_tx: mpsc::Sender<()>, -) { - #[cfg(feature = "rustls-tls")] - let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); - - let worker_url = get_worker_url(); - let ws_url = worker_url - .replace("https://", "wss://") - .replace("http://", "ws://"); - - let url = format!("{}/connect?agent_id={}", ws_url, agent_id); - - info!("Connecting to {}...", url); - - let ws_stream = match tokio::time::timeout( - Duration::from_secs(CONNECTION_TIMEOUT_SECS), - connect_async(url.clone()), - ) - .await - { - Ok(Ok((stream, _))) => { - configure_tcp_keepalive(&stream); - stream - } - Ok(Err(e)) => { - error!("✗ WebSocket connection error: {}", e); - let _ = restart_tx.send(()).await; - return; - } - Err(_) => { - error!( - "✗ WebSocket connection timeout after {}s", - CONNECTION_TIMEOUT_SECS - ); - let _ = restart_tx.send(()).await; - return; - } - }; - - info!("✓ Connected as agent: {}", agent_id); - info!("✓ Proxying to: {}://127.0.0.1:{}", scheme, port); - let public_url = format!("{}/tunnel/{}", worker_url, agent_id); - info!("✓ Public URL: {}", public_url); - - let mut client_builder = reqwest::Client::builder(); - if scheme == "https" { - client_builder = client_builder.danger_accept_invalid_certs(true); - } - let http_client = client_builder - .build() - .expect("failed to build reqwest client"); - - let ctx = ProxyContext { - port, - tunnel_secret, - server_secret, - http_client, - }; - - let (write, read) = ws_stream.split(); - let ws_tx: WebSocketSender = Arc::new(RwLock::new(Some(write))); - let last_activity = Arc::new(RwLock::new(Instant::now())); - let active_tasks: Arc>>> = Arc::new(RwLock::new(Vec::new())); - - let last_activity_clone = last_activity.clone(); - let idle_task = async move { - loop { - tokio::time::sleep(Duration::from_secs(60)).await; - let elapsed = last_activity_clone.read().await.elapsed(); - if elapsed > Duration::from_secs(IDLE_TIMEOUT_SECS) { - warn!( - "No activity for {} minutes, forcing reconnect", - IDLE_TIMEOUT_SECS / 60 - ); - break; - } - } - }; - - tokio::select! { - _ = idle_task => { - info!("✗ Idle timeout triggered"); - } - _ = handle_websocket_messages( - read, - ws_tx.clone(), - ctx, - last_activity, - active_tasks.clone(), - scheme, - ) => { - info!("✗ Connection ended"); - } - } - - cleanup_connection(ws_tx, active_tasks).await; - - let _ = restart_tx.send(()).await; -} - -pub async fn start( - port: u16, - tunnel_secret: String, - server_secret: String, - agent_id: String, - scheme: &str, - handle: Arc>>>, - restart_tx: mpsc::Sender<()>, -) -> Result { - let worker_url = get_worker_url(); - - let agent_id_clone = agent_id.clone(); - let tunnel_secret_clone = tunnel_secret.clone(); - let server_secret_clone = server_secret; - let scheme = scheme.to_string(); - - let task = tokio::spawn(async move { - run_single_connection( - port, - agent_id_clone, - tunnel_secret_clone, - server_secret_clone, - scheme, - restart_tx, - ) - .await; - }); - - *handle.write().await = Some(task); - - let public_url = format!("{}/tunnel/{}", worker_url, agent_id); - let hostname = Url::parse(&worker_url)? - .host_str() - .unwrap_or("") - .to_string(); - - Ok(TunnelInfo { - state: super::TunnelState::Running, - url: public_url, - hostname, - secret: tunnel_secret, - }) -} - -pub async fn stop(handle: Arc>>>) { - if let Some(task) = handle.write().await.take() { - task.abort(); - info!("Lapstone tunnel stopped"); - } -} diff --git a/crates/goose-server/src/tunnel/mod.rs b/crates/goose-server/src/tunnel/mod.rs deleted file mode 100644 index 2b28d362abff..000000000000 --- a/crates/goose-server/src/tunnel/mod.rs +++ /dev/null @@ -1,57 +0,0 @@ -use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, ToSchema)] -#[serde(rename_all = "lowercase")] -pub enum TunnelState { - #[default] - Idle, - Starting, - Running, - Error, - Disabled, -} - -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -pub struct TunnelInfo { - pub state: TunnelState, - pub url: String, - pub hostname: String, - pub secret: String, -} - -pub struct TunnelManager; - -impl Default for TunnelManager { - fn default() -> Self { - Self::new(true) - } -} - -impl TunnelManager { - pub fn new(_tls: bool) -> Self { - TunnelManager - } - - fn is_tunnel_disabled() -> bool { - if let Ok(val) = std::env::var("GOOSE_TUNNEL") { - let val = val.to_lowercase(); - val == "no" || val == "none" - } else { - false - } - } - - pub async fn get_info(&self) -> TunnelInfo { - TunnelInfo { - state: if Self::is_tunnel_disabled() { - TunnelState::Disabled - } else { - TunnelState::Idle - }, - url: String::new(), - hostname: String::new(), - secret: String::new(), - } - } -} diff --git a/crates/goose/acp-meta.json b/crates/goose/acp-meta.json index c496c6089ada..619d7e53ecc1 100644 --- a/crates/goose/acp-meta.json +++ b/crates/goose/acp-meta.json @@ -15,6 +15,11 @@ "requestType": "GetToolsRequest_unstable", "responseType": "GetToolsResponse_unstable" }, + { + "method": "_goose/unstable/tools/permissions/set", + "requestType": "SetToolPermissionsRequest_unstable", + "responseType": "SetToolPermissionsResponse_unstable" + }, { "method": "_goose/unstable/tools/call", "requestType": "GooseToolCallRequest_unstable", @@ -60,6 +65,26 @@ "requestType": "DiagnosticsGetRequest_unstable", "responseType": "DiagnosticsGetResponse_unstable" }, + { + "method": "_goose/unstable/config/prompts/list", + "requestType": "ListPromptsRequest_unstable", + "responseType": "ListPromptsResponse_unstable" + }, + { + "method": "_goose/unstable/config/prompts/get", + "requestType": "GetPromptRequest_unstable", + "responseType": "GetPromptResponse_unstable" + }, + { + "method": "_goose/unstable/config/prompts/save", + "requestType": "SavePromptRequest_unstable", + "responseType": "PromptOperationResponse_unstable" + }, + { + "method": "_goose/unstable/config/prompts/reset", + "requestType": "ResetPromptRequest_unstable", + "responseType": "PromptOperationResponse_unstable" + }, { "method": "session/delete", "requestType": "DeleteSessionRequest", @@ -170,6 +195,21 @@ "requestType": "ProviderConfigAuthenticateRequest_unstable", "responseType": "ProviderConfigChangeResponse_unstable" }, + { + "method": "_goose/unstable/providers/secrets/list", + "requestType": "ProviderSecretsListRequest_unstable", + "responseType": "ProviderSecretsListResponse_unstable" + }, + { + "method": "_goose/unstable/providers/secrets/delete", + "requestType": "ProviderSecretDeleteRequest_unstable", + "responseType": "EmptyResponse" + }, + { + "method": "_goose/unstable/providers/canonical-model-info", + "requestType": "CanonicalModelInfoRequest_unstable", + "responseType": "CanonicalModelInfoResponse_unstable" + }, { "method": "_goose/unstable/preferences/read", "requestType": "PreferencesReadRequest_unstable", @@ -185,6 +225,26 @@ "requestType": "PreferencesRemoveRequest_unstable", "responseType": "EmptyResponse" }, + { + "method": "_goose/unstable/config/read", + "requestType": "ConfigReadRequest_unstable", + "responseType": "ConfigReadResponse_unstable" + }, + { + "method": "_goose/unstable/config/upsert", + "requestType": "ConfigUpsertRequest_unstable", + "responseType": "EmptyResponse" + }, + { + "method": "_goose/unstable/config/remove", + "requestType": "ConfigRemoveRequest_unstable", + "responseType": "EmptyResponse" + }, + { + "method": "_goose/unstable/config/read-all", + "requestType": "ConfigReadAllRequest_unstable", + "responseType": "ConfigReadAllResponse_unstable" + }, { "method": "_goose/unstable/defaults/read", "requestType": "DefaultsReadRequest_unstable", @@ -195,6 +255,11 @@ "requestType": "DefaultsSaveRequest_unstable", "responseType": "DefaultsReadResponse_unstable" }, + { + "method": "_goose/unstable/defaults/clear", + "requestType": "DefaultsClearRequest_unstable", + "responseType": "DefaultsReadResponse_unstable" + }, { "method": "_goose/unstable/onboarding/import/scan", "requestType": "OnboardingImportScanRequest_unstable", diff --git a/crates/goose/acp-schema.json b/crates/goose/acp-schema.json index 77d74f28baa5..cf9f13a3df43 100644 --- a/crates/goose/acp-schema.json +++ b/crates/goose/acp-schema.json @@ -383,6 +383,13 @@ "properties": { "sessionId": { "type": "string" + }, + "extensionName": { + "type": [ + "string", + "null" + ], + "description": "Filter tools to those belonging to this extension." } }, "required": [ @@ -397,8 +404,9 @@ "properties": { "tools": { "type": "array", - "items": {}, - "description": "Array of tool info objects with `name`, `description`, `parameters`, and optional `permission`." + "items": { + "$ref": "#/$defs/ToolListItem" + } } }, "required": [ @@ -408,6 +416,89 @@ "x-side": "agent", "x-method": "_goose/unstable/tools/list" }, + "ToolListItem": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "type": "string" + } + }, + "permission": { + "anyOf": [ + { + "$ref": "#/$defs/ToolPermissionLevel" + }, + { + "type": "null" + } + ] + }, + "inputSchema": {}, + "outputSchema": {} + }, + "required": [ + "name", + "description", + "parameters", + "inputSchema" + ], + "description": "A single tool item returned by the tools list endpoint." + }, + "ToolPermissionLevel": { + "type": "string", + "enum": [ + "always_allow", + "ask_before", + "never_allow" + ], + "description": "Permission level for a tool." + }, + "SetToolPermissionsRequest_unstable": { + "type": "object", + "properties": { + "toolPermissions": { + "type": "array", + "items": { + "$ref": "#/$defs/ToolPermissionEntry" + } + } + }, + "required": [ + "toolPermissions" + ], + "description": "Set permission levels for one or more tools.", + "x-side": "agent", + "x-method": "_goose/unstable/tools/permissions/set" + }, + "ToolPermissionEntry": { + "type": "object", + "properties": { + "toolName": { + "type": "string" + }, + "permission": { + "$ref": "#/$defs/ToolPermissionLevel" + } + }, + "required": [ + "toolName", + "permission" + ], + "description": "A single tool permission entry." + }, + "SetToolPermissionsResponse_unstable": { + "type": "object", + "x-side": "agent", + "x-method": "_goose/unstable/tools/permissions/set" + }, "GooseToolCallRequest_unstable": { "type": "object", "properties": { @@ -1089,6 +1180,141 @@ "x-side": "agent", "x-method": "_goose/unstable/diagnostics/get" }, + "ListPromptsRequest_unstable": { + "type": "object", + "description": "List all available Goose prompt templates.", + "x-side": "agent", + "x-method": "_goose/unstable/config/prompts/list" + }, + "ListPromptsResponse_unstable": { + "type": "object", + "properties": { + "prompts": { + "type": "array", + "items": { + "$ref": "#/$defs/PromptTemplateEntry" + } + } + }, + "required": [ + "prompts" + ], + "x-side": "agent", + "x-method": "_goose/unstable/config/prompts/list" + }, + "PromptTemplateEntry": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "defaultContent": { + "type": "string" + }, + "userContent": { + "type": [ + "string", + "null" + ] + }, + "isCustomized": { + "type": "boolean" + } + }, + "required": [ + "name", + "description", + "defaultContent", + "isCustomized" + ], + "description": "Information about a prompt template, including its default content and customization status." + }, + "GetPromptRequest_unstable": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "description": "Read a Goose prompt template.", + "x-side": "agent", + "x-method": "_goose/unstable/config/prompts/get" + }, + "GetPromptResponse_unstable": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "content": { + "type": "string" + }, + "defaultContent": { + "type": "string" + }, + "isCustomized": { + "type": "boolean" + } + }, + "required": [ + "name", + "content", + "defaultContent", + "isCustomized" + ], + "x-side": "agent", + "x-method": "_goose/unstable/config/prompts/get" + }, + "SavePromptRequest_unstable": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "content": { + "type": "string" + } + }, + "required": [ + "name", + "content" + ], + "description": "Save a custom Goose prompt template.", + "x-side": "agent", + "x-method": "_goose/unstable/config/prompts/save" + }, + "PromptOperationResponse_unstable": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "x-side": "agent" + }, + "ResetPromptRequest_unstable": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "description": "Reset a Goose prompt template to its default content.", + "x-side": "agent", + "x-method": "_goose/unstable/config/prompts/reset" + }, "DeleteSessionRequest": { "type": "object", "properties": { @@ -2421,26 +2647,234 @@ "type": "string" } }, - "required": [ - "providerId" - ], - "description": "Delete provider configuration fields and start an inventory refresh when supported.", + "required": [ + "providerId" + ], + "description": "Delete provider configuration fields and start an inventory refresh when supported.", + "x-side": "agent", + "x-method": "_goose/unstable/providers/config/delete" + }, + "ProviderConfigAuthenticateRequest_unstable": { + "type": "object", + "properties": { + "providerId": { + "type": "string" + } + }, + "required": [ + "providerId" + ], + "description": "Run a provider-owned native authentication flow and start an inventory refresh when supported.", + "x-side": "agent", + "x-method": "_goose/unstable/providers/config/authenticate" + }, + "ProviderSecretsListRequest_unstable": { + "type": "object", + "description": "List provider credentials stored locally by Goose.", + "x-side": "agent", + "x-method": "_goose/unstable/providers/secrets/list" + }, + "ProviderSecretsListResponse_unstable": { + "type": "object", + "properties": { + "secrets": { + "type": "array", + "items": { + "$ref": "#/$defs/ProviderSecretDto" + } + } + }, + "required": [ + "secrets" + ], + "x-side": "agent", + "x-method": "_goose/unstable/providers/secrets/list" + }, + "ProviderSecretDto": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "providerDisplayName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "storage": { + "$ref": "#/$defs/ProviderSecretStorageDto" + }, + "expiresAt": { + "type": [ + "string", + "null" + ] + }, + "status": { + "$ref": "#/$defs/ProviderSecretStatusDto" + }, + "configured": { + "type": "boolean" + }, + "hasSecret": { + "type": "boolean" + }, + "canDelete": { + "type": "boolean" + }, + "canConfigure": { + "type": "boolean" + }, + "configureProvider": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "id", + "provider", + "providerDisplayName", + "name", + "storage", + "status", + "configured", + "hasSecret", + "canDelete", + "canConfigure" + ] + }, + "ProviderSecretStorageDto": { + "type": "string", + "enum": [ + "secret_store", + "provider_cache" + ] + }, + "ProviderSecretStatusDto": { + "type": "string", + "enum": [ + "valid", + "expired", + "unknown" + ] + }, + "ProviderSecretDeleteRequest_unstable": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "description": "Delete a locally stored provider credential by id.", + "x-side": "agent", + "x-method": "_goose/unstable/providers/secrets/delete" + }, + "CanonicalModelInfoRequest_unstable": { + "type": "object", + "properties": { + "provider": { + "type": "string" + }, + "model": { + "type": "string" + } + }, + "required": [ + "provider", + "model" + ], + "description": "Look up canonical (bundled-registry) model info for a provider/model pair.", + "x-side": "agent", + "x-method": "_goose/unstable/providers/canonical-model-info" + }, + "CanonicalModelInfoResponse_unstable": { + "type": "object", + "properties": { + "modelInfo": { + "anyOf": [ + { + "$ref": "#/$defs/CanonicalModelInfoDto" + }, + { + "type": "null" + } + ] + } + }, "x-side": "agent", - "x-method": "_goose/unstable/providers/config/delete" + "x-method": "_goose/unstable/providers/canonical-model-info" }, - "ProviderConfigAuthenticateRequest_unstable": { + "CanonicalModelInfoDto": { "type": "object", "properties": { - "providerId": { + "provider": { + "type": "string" + }, + "model": { + "type": "string" + }, + "contextLimit": { + "type": "integer", + "minimum": 0 + }, + "maxOutputTokens": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "reasoning": { + "type": "boolean" + }, + "inputTokenCost": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "outputTokenCost": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "cacheReadTokenCost": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "cacheWriteTokenCost": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "currency": { "type": "string" } }, "required": [ - "providerId" - ], - "description": "Run a provider-owned native authentication flow and start an inventory refresh when supported.", - "x-side": "agent", - "x-method": "_goose/unstable/providers/config/authenticate" + "provider", + "model", + "contextLimit", + "reasoning", + "currency" + ] }, "PreferencesReadRequest_unstable": { "type": "object", @@ -2527,6 +2961,88 @@ "x-side": "agent", "x-method": "_goose/unstable/preferences/remove" }, + "ConfigReadRequest_unstable": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "isSecret": { + "type": "boolean", + "default": false + } + }, + "required": [ + "key" + ], + "x-side": "agent", + "x-method": "_goose/unstable/config/read" + }, + "ConfigReadResponse_unstable": { + "type": "object", + "properties": { + "value": { + "default": null + } + }, + "x-side": "agent", + "x-method": "_goose/unstable/config/read" + }, + "ConfigUpsertRequest_unstable": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "value": {}, + "isSecret": { + "type": "boolean", + "default": false + } + }, + "required": [ + "key", + "value" + ], + "x-side": "agent", + "x-method": "_goose/unstable/config/upsert" + }, + "ConfigRemoveRequest_unstable": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "isSecret": { + "type": "boolean", + "default": false + } + }, + "required": [ + "key" + ], + "x-side": "agent", + "x-method": "_goose/unstable/config/remove" + }, + "ConfigReadAllRequest_unstable": { + "type": "object", + "x-side": "agent", + "x-method": "_goose/unstable/config/read-all" + }, + "ConfigReadAllResponse_unstable": { + "type": "object", + "properties": { + "config": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "config" + ], + "x-side": "agent", + "x-method": "_goose/unstable/config/read-all" + }, "DefaultsReadRequest_unstable": { "type": "object", "description": "Read Goose default provider and model configuration.", @@ -2571,6 +3087,12 @@ "x-side": "agent", "x-method": "_goose/unstable/defaults/save" }, + "DefaultsClearRequest_unstable": { + "type": "object", + "description": "Clear Goose default provider and model configuration.", + "x-side": "agent", + "x-method": "_goose/unstable/defaults/clear" + }, "OnboardingImportScanRequest_unstable": { "type": "object", "properties": { @@ -5225,6 +5747,15 @@ "description": "Params for _goose/unstable/tools/list", "title": "GetToolsRequest_unstable" }, + { + "allOf": [ + { + "$ref": "#/$defs/SetToolPermissionsRequest_unstable" + } + ], + "description": "Params for _goose/unstable/tools/permissions/set", + "title": "SetToolPermissionsRequest_unstable" + }, { "allOf": [ { @@ -5306,6 +5837,42 @@ "description": "Params for _goose/unstable/diagnostics/get", "title": "DiagnosticsGetRequest_unstable" }, + { + "allOf": [ + { + "$ref": "#/$defs/ListPromptsRequest_unstable" + } + ], + "description": "Params for _goose/unstable/config/prompts/list", + "title": "ListPromptsRequest_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/GetPromptRequest_unstable" + } + ], + "description": "Params for _goose/unstable/config/prompts/get", + "title": "GetPromptRequest_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/SavePromptRequest_unstable" + } + ], + "description": "Params for _goose/unstable/config/prompts/save", + "title": "SavePromptRequest_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/ResetPromptRequest_unstable" + } + ], + "description": "Params for _goose/unstable/config/prompts/reset", + "title": "ResetPromptRequest_unstable" + }, { "allOf": [ { @@ -5504,6 +6071,33 @@ "description": "Params for _goose/unstable/providers/config/authenticate", "title": "ProviderConfigAuthenticateRequest_unstable" }, + { + "allOf": [ + { + "$ref": "#/$defs/ProviderSecretsListRequest_unstable" + } + ], + "description": "Params for _goose/unstable/providers/secrets/list", + "title": "ProviderSecretsListRequest_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/ProviderSecretDeleteRequest_unstable" + } + ], + "description": "Params for _goose/unstable/providers/secrets/delete", + "title": "ProviderSecretDeleteRequest_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/CanonicalModelInfoRequest_unstable" + } + ], + "description": "Params for _goose/unstable/providers/canonical-model-info", + "title": "CanonicalModelInfoRequest_unstable" + }, { "allOf": [ { @@ -5531,6 +6125,42 @@ "description": "Params for _goose/unstable/preferences/remove", "title": "PreferencesRemoveRequest_unstable" }, + { + "allOf": [ + { + "$ref": "#/$defs/ConfigReadRequest_unstable" + } + ], + "description": "Params for _goose/unstable/config/read", + "title": "ConfigReadRequest_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/ConfigUpsertRequest_unstable" + } + ], + "description": "Params for _goose/unstable/config/upsert", + "title": "ConfigUpsertRequest_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/ConfigRemoveRequest_unstable" + } + ], + "description": "Params for _goose/unstable/config/remove", + "title": "ConfigRemoveRequest_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/ConfigReadAllRequest_unstable" + } + ], + "description": "Params for _goose/unstable/config/read-all", + "title": "ConfigReadAllRequest_unstable" + }, { "allOf": [ { @@ -5549,6 +6179,15 @@ "description": "Params for _goose/unstable/defaults/save", "title": "DefaultsSaveRequest_unstable" }, + { + "allOf": [ + { + "$ref": "#/$defs/DefaultsClearRequest_unstable" + } + ], + "description": "Params for _goose/unstable/defaults/clear", + "title": "DefaultsClearRequest_unstable" + }, { "allOf": [ { @@ -6036,6 +6675,14 @@ ], "title": "GetToolsResponse_unstable" }, + { + "allOf": [ + { + "$ref": "#/$defs/SetToolPermissionsResponse_unstable" + } + ], + "title": "SetToolPermissionsResponse_unstable" + }, { "allOf": [ { @@ -6092,6 +6739,30 @@ ], "title": "DiagnosticsGetResponse_unstable" }, + { + "allOf": [ + { + "$ref": "#/$defs/ListPromptsResponse_unstable" + } + ], + "title": "ListPromptsResponse_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/GetPromptResponse_unstable" + } + ], + "title": "GetPromptResponse_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/PromptOperationResponse_unstable" + } + ], + "title": "PromptOperationResponse_unstable" + }, { "allOf": [ { @@ -6220,6 +6891,22 @@ ], "title": "ProviderConfigChangeResponse_unstable" }, + { + "allOf": [ + { + "$ref": "#/$defs/ProviderSecretsListResponse_unstable" + } + ], + "title": "ProviderSecretsListResponse_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/CanonicalModelInfoResponse_unstable" + } + ], + "title": "CanonicalModelInfoResponse_unstable" + }, { "allOf": [ { @@ -6228,6 +6915,22 @@ ], "title": "PreferencesReadResponse_unstable" }, + { + "allOf": [ + { + "$ref": "#/$defs/ConfigReadResponse_unstable" + } + ], + "title": "ConfigReadResponse_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/ConfigReadAllResponse_unstable" + } + ], + "title": "ConfigReadAllResponse_unstable" + }, { "allOf": [ { diff --git a/crates/goose/src/acp/server.rs b/crates/goose/src/acp/server.rs index 7eef660585f6..ee83fa0e4978 100644 --- a/crates/goose/src/acp/server.rs +++ b/crates/goose/src/acp/server.rs @@ -97,6 +97,7 @@ mod load_session; mod manage_sessions; mod new_session; mod onboarding; +mod prompts; mod providers; mod recipe; mod resources; diff --git a/crates/goose/src/acp/server/config.rs b/crates/goose/src/acp/server/config.rs index 654af78c4ab4..e70ddf98dfb1 100644 --- a/crates/goose/src/acp/server/config.rs +++ b/crates/goose/src/acp/server/config.rs @@ -1,6 +1,22 @@ use super::*; use goose_providers::thinking::ThinkingEffort; +const SECRET_MASK_SHOW_LEN: usize = 8; + +fn mask_secret(secret: serde_json::Value) -> String { + let as_string = match secret { + serde_json::Value::String(s) => s, + _ => serde_json::to_string(&secret).unwrap_or_else(|_| secret.to_string()), + }; + + let chars: Vec<_> = as_string.chars().collect(); + let show_len = std::cmp::min(chars.len() / 2, SECRET_MASK_SHOW_LEN); + let visible: String = chars.iter().take(show_len).collect(); + let mask = "*".repeat(chars.len() - show_len); + + format!("{}{}", visible, mask) +} + impl GooseAcpAgent { pub(super) async fn on_preferences_read( &self, @@ -58,6 +74,101 @@ impl GooseAcpAgent { Ok(EmptyResponse {}) } + pub(super) async fn on_config_read( + &self, + req: ConfigReadRequest, + ) -> Result { + let config = self.config()?; + + if req.key == "GOOSE_PROVIDER" || req.key == "active_provider" { + let value = config + .get_goose_provider() + .map(serde_json::Value::String) + .unwrap_or(serde_json::Value::Null); + return Ok(ConfigReadResponse { value }); + } + if req.key == "GOOSE_MODEL" { + let value = config + .get_goose_model() + .map(serde_json::Value::String) + .unwrap_or(serde_json::Value::Null); + return Ok(ConfigReadResponse { value }); + } + + let value = match config.get(&req.key, req.is_secret) { + Ok(value) if req.is_secret => serde_json::Value::String(mask_secret(value)), + Ok(value) => value, + Err(crate::config::ConfigError::NotFound(_)) => serde_json::Value::Null, + Err(e) => { + return Err(agent_client_protocol::Error::internal_error().data(e.to_string())) + } + }; + Ok(ConfigReadResponse { value }) + } + + pub(super) async fn on_config_upsert( + &self, + req: ConfigUpsertRequest, + ) -> Result { + let config = self.config()?; + + if req.key == "GOOSE_PROVIDER" { + if let Some(name) = req.value.as_str() { + let model = crate::config::get_provider_entry(config, name) + .map(|e| e.model) + .or_else(|| config.get_goose_model().ok()) + .unwrap_or_default(); + crate::config::set_active_provider(config, name, &model).internal_err()?; + return Ok(EmptyResponse {}); + } + } + if req.key == "GOOSE_MODEL" { + if let Some(model) = req.value.as_str() { + if let Ok(provider) = config.get_goose_provider() { + crate::config::set_active_provider(config, &provider, model).internal_err()?; + return Ok(EmptyResponse {}); + } + } + } + + config + .set(&req.key, &req.value, req.is_secret) + .internal_err()?; + Ok(EmptyResponse {}) + } + + pub(super) async fn on_config_remove( + &self, + req: ConfigRemoveRequest, + ) -> Result { + let config = self.config()?; + + if req.is_secret { + config.delete_secret(&req.key).internal_err()?; + } else if req.key == "GOOSE_PROVIDER" || req.key == "active_provider" { + config.delete("active_provider").internal_err()?; + config.delete("GOOSE_PROVIDER").internal_err()?; + } else if req.key == "GOOSE_MODEL" { + if let Ok(provider) = config.get_goose_provider() { + crate::config::set_active_provider(config, &provider, "").internal_err()?; + } + config.delete("GOOSE_MODEL").internal_err()?; + } else { + config.delete(&req.key).internal_err()?; + } + + Ok(EmptyResponse {}) + } + + pub(super) async fn on_config_read_all( + &self, + _req: ConfigReadAllRequest, + ) -> Result { + let config = self.config()?; + let values = config.all_values().internal_err()?; + Ok(ConfigReadAllResponse { config: values }) + } + pub(super) async fn on_defaults_read( &self, _req: DefaultsReadRequest, @@ -127,6 +238,20 @@ impl GooseAcpAgent { model_id, }) } + + pub(super) async fn on_defaults_clear( + &self, + _req: DefaultsClearRequest, + ) -> Result { + let config = self.config()?; + crate::config::clear_active_provider(config) + .internal_err_ctx("Failed to clear default provider")?; + + Ok(DefaultsReadResponse { + provider_id: None, + model_id: None, + }) + } } struct PreferenceDef { diff --git a/crates/goose/src/acp/server/custom_dispatch.rs b/crates/goose/src/acp/server/custom_dispatch.rs index 50a3d3da3162..b15693746d1a 100644 --- a/crates/goose/src/acp/server/custom_dispatch.rs +++ b/crates/goose/src/acp/server/custom_dispatch.rs @@ -42,6 +42,14 @@ impl GooseAcpAgent { self.on_get_tools(req).await } + #[custom_method(SetToolPermissionsRequest)] + async fn dispatch_set_tool_permissions( + &self, + req: SetToolPermissionsRequest, + ) -> Result { + self.on_set_tool_permissions(req).await + } + #[custom_method(GooseToolCallRequest)] async fn dispatch_call_tool( &self, @@ -114,6 +122,38 @@ impl GooseAcpAgent { self.on_get_diagnostics(req).await } + #[custom_method(ListPromptsRequest)] + async fn dispatch_list_prompts( + &self, + req: ListPromptsRequest, + ) -> Result { + self.on_list_prompts(req).await + } + + #[custom_method(GetPromptRequest)] + async fn dispatch_get_prompt( + &self, + req: GetPromptRequest, + ) -> Result { + self.on_get_prompt(req).await + } + + #[custom_method(SavePromptRequest)] + async fn dispatch_save_prompt( + &self, + req: SavePromptRequest, + ) -> Result { + self.on_save_prompt(req).await + } + + #[custom_method(ResetPromptRequest)] + async fn dispatch_reset_prompt( + &self, + req: ResetPromptRequest, + ) -> Result { + self.on_reset_prompt(req).await + } + #[custom_method(DeleteSessionRequest)] async fn dispatch_delete_session( &self, @@ -288,6 +328,30 @@ impl GooseAcpAgent { self.on_authenticate_provider_config(req).await } + #[custom_method(ProviderSecretsListRequest)] + async fn dispatch_list_provider_secrets( + &self, + req: ProviderSecretsListRequest, + ) -> Result { + self.on_list_provider_secrets(req).await + } + + #[custom_method(ProviderSecretDeleteRequest)] + async fn dispatch_delete_provider_secret( + &self, + req: ProviderSecretDeleteRequest, + ) -> Result { + self.on_delete_provider_secret(req).await + } + + #[custom_method(CanonicalModelInfoRequest)] + async fn dispatch_canonical_model_info( + &self, + req: CanonicalModelInfoRequest, + ) -> Result { + self.on_canonical_model_info(req).await + } + #[custom_method(PreferencesReadRequest)] async fn dispatch_preferences_read( &self, @@ -312,6 +376,38 @@ impl GooseAcpAgent { self.on_preferences_remove(req).await } + #[custom_method(ConfigReadRequest)] + async fn dispatch_config_read( + &self, + req: ConfigReadRequest, + ) -> Result { + self.on_config_read(req).await + } + + #[custom_method(ConfigUpsertRequest)] + async fn dispatch_config_upsert( + &self, + req: ConfigUpsertRequest, + ) -> Result { + self.on_config_upsert(req).await + } + + #[custom_method(ConfigRemoveRequest)] + async fn dispatch_config_remove( + &self, + req: ConfigRemoveRequest, + ) -> Result { + self.on_config_remove(req).await + } + + #[custom_method(ConfigReadAllRequest)] + async fn dispatch_config_read_all( + &self, + req: ConfigReadAllRequest, + ) -> Result { + self.on_config_read_all(req).await + } + #[custom_method(DefaultsReadRequest)] async fn dispatch_defaults_read( &self, @@ -328,6 +424,14 @@ impl GooseAcpAgent { self.on_defaults_save(req).await } + #[custom_method(DefaultsClearRequest)] + async fn dispatch_defaults_clear( + &self, + req: DefaultsClearRequest, + ) -> Result { + self.on_defaults_clear(req).await + } + #[custom_method(OnboardingImportScanRequest)] async fn dispatch_onboarding_import_scan( &self, diff --git a/crates/goose/src/acp/server/prompts.rs b/crates/goose/src/acp/server/prompts.rs new file mode 100644 index 000000000000..7051f792ac0c --- /dev/null +++ b/crates/goose/src/acp/server/prompts.rs @@ -0,0 +1,81 @@ +use super::*; +use crate::prompt_template::{get_template, list_templates, reset_template, save_template}; + +impl GooseAcpAgent { + pub(super) async fn on_list_prompts( + &self, + _req: ListPromptsRequest, + ) -> Result { + let prompts = list_templates() + .into_iter() + .map(prompt_template_to_entry) + .collect(); + + Ok(ListPromptsResponse { prompts }) + } + + pub(super) async fn on_get_prompt( + &self, + req: GetPromptRequest, + ) -> Result { + let template = get_template(&req.name).ok_or_else(|| prompt_not_found(&req.name))?; + let content = template + .user_content + .as_ref() + .unwrap_or(&template.default_content) + .clone(); + + Ok(GetPromptResponse { + name: template.name, + content, + default_content: template.default_content, + is_customized: template.is_customized, + }) + } + + pub(super) async fn on_save_prompt( + &self, + req: SavePromptRequest, + ) -> Result { + save_template(&req.name, &req.content).map_err(|err| prompt_io_error(&req.name, err))?; + + Ok(PromptOperationResponse { + message: format!("Saved prompt: {}", req.name), + }) + } + + pub(super) async fn on_reset_prompt( + &self, + req: ResetPromptRequest, + ) -> Result { + reset_template(&req.name).map_err(|err| prompt_io_error(&req.name, err))?; + + Ok(PromptOperationResponse { + message: format!("Reset prompt to default: {}", req.name), + }) + } +} + +fn prompt_template_to_entry(template: crate::prompt_template::Template) -> PromptTemplateEntry { + PromptTemplateEntry { + name: template.name, + description: template.description, + default_content: template.default_content, + user_content: template.user_content, + is_customized: template.is_customized, + } +} + +fn prompt_not_found(name: &str) -> agent_client_protocol::Error { + agent_client_protocol::Error::invalid_params() + .data(format!("Prompt template '{name}' not found")) +} + +fn prompt_io_error(name: &str, err: std::io::Error) -> agent_client_protocol::Error { + if err.kind() == std::io::ErrorKind::NotFound { + prompt_not_found(name) + } else { + agent_client_protocol::Error::internal_error() + .data(format!("Failed to update prompt '{name}': {err}")) + } +} diff --git a/crates/goose/src/acp/server/providers.rs b/crates/goose/src/acp/server/providers.rs index 342d647d73f3..d1a496137f0c 100644 --- a/crates/goose/src/acp/server/providers.rs +++ b/crates/goose/src/acp/server/providers.rs @@ -1,8 +1,39 @@ use super::*; use crate::config::declarative_providers; use crate::providers::inventory::ensure_refresh_identity_current; +use crate::providers::provider_secrets; use std::str::FromStr; +fn provider_secret_to_dto(secret: provider_secrets::ProviderSecret) -> ProviderSecretDto { + let storage = match secret.storage { + provider_secrets::ProviderSecretStorage::SecretStore => { + ProviderSecretStorageDto::SecretStore + } + provider_secrets::ProviderSecretStorage::ProviderCache => { + ProviderSecretStorageDto::ProviderCache + } + }; + let status = match secret.status { + provider_secrets::ProviderSecretStatus::Valid => ProviderSecretStatusDto::Valid, + provider_secrets::ProviderSecretStatus::Expired => ProviderSecretStatusDto::Expired, + provider_secrets::ProviderSecretStatus::Unknown => ProviderSecretStatusDto::Unknown, + }; + ProviderSecretDto { + id: secret.id, + provider: secret.provider, + provider_display_name: secret.provider_display_name, + name: secret.name, + storage, + expires_at: secret.expires_at.map(|value| value.to_rfc3339()), + status, + configured: secret.configured, + has_secret: secret.has_secret, + can_delete: secret.can_delete, + can_configure: secret.can_configure, + configure_provider: secret.configure_provider, + } +} + fn inventory_entry_to_dto(entry: ProviderInventoryEntry) -> ProviderInventoryEntryDto { let stale = ProviderInventoryService::is_stale(&entry); ProviderInventoryEntryDto { @@ -927,22 +958,29 @@ impl GooseAcpAgent { let entry = crate::providers::get_from_registry(&req.provider_id) .await .invalid_params_err_ctx("Unknown provider")?; - let metadata = entry.metadata().clone(); - if !metadata.config_keys.iter().any(|key| key.oauth_flow) { - return Err(agent_client_protocol::Error::invalid_params().data(format!( - "Provider does not support native authentication: {}", - req.provider_id - ))); - } - let provider = entry - .create_with_default_model(Vec::new()) - .await - .internal_err_ctx("Failed to initialize provider")?; - provider - .configure_oauth() - .await - .internal_err_ctx("Failed to authenticate provider")?; + if req.provider_id == crate::providers::huggingface_auth::HUGGINGFACE_PROVIDER_NAME { + crate::providers::huggingface_auth::configure_oauth() + .await + .internal_err_ctx("Failed to authenticate provider")?; + } else { + let metadata = entry.metadata().clone(); + if !metadata.config_keys.iter().any(|key| key.oauth_flow) { + return Err(agent_client_protocol::Error::invalid_params().data(format!( + "Provider does not support native authentication: {}", + req.provider_id + ))); + } + + let provider = entry + .create_with_default_model(Vec::new()) + .await + .internal_err_ctx("Failed to initialize provider")?; + provider + .configure_oauth() + .await + .internal_err_ctx("Failed to authenticate provider")?; + } Config::global().invalidate_secrets_cache(); let provider_ids = [req.provider_id.clone()]; @@ -950,4 +988,58 @@ impl GooseAcpAgent { let refresh = self.start_provider_inventory_refresh(&provider_ids).await?; Ok(ProviderConfigChangeResponse { status, refresh }) } + + pub(super) async fn on_list_provider_secrets( + &self, + _req: ProviderSecretsListRequest, + ) -> Result { + let secrets = provider_secrets::list_provider_secrets() + .await + .internal_err_ctx("Failed to list provider secrets")? + .into_iter() + .map(provider_secret_to_dto) + .collect(); + Ok(ProviderSecretsListResponse { secrets }) + } + + pub(super) async fn on_delete_provider_secret( + &self, + req: ProviderSecretDeleteRequest, + ) -> Result { + match provider_secrets::delete_provider_secret(&req.id).await { + Ok(()) => Ok(EmptyResponse {}), + Err(provider_secrets::DeleteProviderSecretError::InvalidId(id)) => { + Err(agent_client_protocol::Error::invalid_params() + .data(format!("Invalid provider secret id: '{}'", id))) + } + Err(e) => Err(agent_client_protocol::Error::internal_error().data(e.to_string())), + } + } + + pub(super) async fn on_canonical_model_info( + &self, + req: CanonicalModelInfoRequest, + ) -> Result { + use goose_providers::model::ModelConfig; + + let model_info = + crate::providers::canonical::maybe_get_canonical_model(&req.provider, &req.model).map( + |canonical_model| CanonicalModelInfoDto { + provider: req.provider.clone(), + model: req.model.clone(), + context_limit: canonical_model.limit.context, + max_output_tokens: canonical_model.limit.output, + reasoning: canonical_model + .reasoning + .unwrap_or_else(|| ModelConfig::new(&req.model).is_reasoning_model()), + input_token_cost: canonical_model.cost.input, + output_token_cost: canonical_model.cost.output, + cache_read_token_cost: canonical_model.cost.cache_read, + cache_write_token_cost: canonical_model.cost.cache_write, + currency: "$".to_string(), + }, + ); + + Ok(CanonicalModelInfoResponse { model_info }) + } } diff --git a/crates/goose/src/acp/server/tools.rs b/crates/goose/src/acp/server/tools.rs index 24b58acac748..654a2b36afbc 100644 --- a/crates/goose/src/acp/server/tools.rs +++ b/crates/goose/src/acp/server/tools.rs @@ -1,5 +1,8 @@ use super::*; +use crate::agents::extension_manager::get_parameter_names; use crate::agents::reply_parts::is_tool_visible_to_app; +use crate::config::permission::PermissionLevel; +use goose_sdk_types::custom_requests::{ToolListItem, ToolPermissionLevel}; use rmcp::model::CallToolRequestParams; impl GooseAcpAgent { @@ -9,13 +12,51 @@ impl GooseAcpAgent { ) -> Result { let session_id = &req.session_id; let agent = self.get_session_agent(&req.session_id).await?; - let tools = agent.list_tools(session_id, None).await; - let tools_json = tools + let goose_mode = agent.goose_mode().await; + // Read from the global static manager so REST-based confirmToolAction approvals + // (which update PermissionManager::instance()) are reflected here immediately. + let permission_manager = crate::config::PermissionManager::instance(); + + let mut tools: Vec = agent + .list_tools(session_id, req.extension_name) + .await .into_iter() - .map(|t| serde_json::to_value(&t)) - .collect::, _>>() - .internal_err()?; - Ok(GetToolsResponse { tools: tools_json }) + .map(|tool| { + let permission = permission_manager + .get_user_permission(&tool.name) + .or_else(|| { + if goose_mode == GooseMode::SmartApprove { + permission_manager.get_smart_approve_permission(&tool.name) + } else if goose_mode == GooseMode::Approve { + Some(PermissionLevel::AskBefore) + } else { + None + } + }) + .map(|p| match p { + PermissionLevel::AlwaysAllow => ToolPermissionLevel::AlwaysAllow, + PermissionLevel::AskBefore => ToolPermissionLevel::AskBefore, + PermissionLevel::NeverAllow => ToolPermissionLevel::NeverAllow, + }); + ToolListItem { + name: tool.name.to_string(), + description: tool + .description + .as_ref() + .map(|d| d.as_ref().to_string()) + .unwrap_or_default(), + parameters: get_parameter_names(&tool), + permission, + input_schema: serde_json::Value::Object(tool.input_schema.as_ref().clone()), + output_schema: tool + .output_schema + .as_ref() + .map(|s| serde_json::to_value(s).unwrap_or(serde_json::Value::Null)), + } + }) + .collect(); + tools.sort_by(|a, b| a.name.cmp(&b.name)); + Ok(GetToolsResponse { tools }) } pub(super) async fn on_call_tool( @@ -78,4 +119,23 @@ impl GooseAcpAgent { meta: result.meta.and_then(|m| serde_json::to_value(m).ok()), }) } + + pub(super) async fn on_set_tool_permissions( + &self, + req: SetToolPermissionsRequest, + ) -> Result { + let acp_permission_manager = self.permission_manager(); + // Also update the global static manager used by HTTP agents when USE_ACP_CHAT is false. + let global_permission_manager = crate::config::PermissionManager::instance(); + for entry in &req.tool_permissions { + let level = match entry.permission { + ToolPermissionLevel::AlwaysAllow => PermissionLevel::AlwaysAllow, + ToolPermissionLevel::AskBefore => PermissionLevel::AskBefore, + ToolPermissionLevel::NeverAllow => PermissionLevel::NeverAllow, + }; + acp_permission_manager.update_user_permission(&entry.tool_name, level.clone()); + global_permission_manager.update_user_permission(&entry.tool_name, level); + } + Ok(SetToolPermissionsResponse {}) + } } diff --git a/crates/goose/src/config/mod.rs b/crates/goose/src/config/mod.rs index 13abeec0077a..cfe7a814f01e 100644 --- a/crates/goose/src/config/mod.rs +++ b/crates/goose/src/config/mod.rs @@ -32,6 +32,6 @@ pub use extensions::DEFAULT_EXTENSION; pub use extensions::DEFAULT_EXTENSION_DESCRIPTION; pub use extensions::DEFAULT_EXTENSION_TIMEOUT; pub use providers::{ - get_active_model, get_active_provider, get_provider_entry, set_active_provider, - set_provider_entry, ProviderEntry, + clear_active_provider, get_active_model, get_active_provider, get_provider_entry, + set_active_provider, set_provider_entry, ProviderEntry, }; diff --git a/crates/goose/src/config/providers.rs b/crates/goose/src/config/providers.rs index fd7dd14fc3b8..c8068bad2559 100644 --- a/crates/goose/src/config/providers.rs +++ b/crates/goose/src/config/providers.rs @@ -8,11 +8,6 @@ use tracing::warn; const PROVIDERS_CONFIG_KEY: &str = "providers"; const ACTIVE_PROVIDER_KEY: &str = "active_provider"; -/// A single provider's persisted configuration within the `providers:` block. -/// -/// The `providers` block in config.yaml is the authoritative source for -/// per-provider settings, replacing the old flat-key scheme where switching -/// providers destructively overwrote `GOOSE_PROVIDER` / `GOOSE_MODEL`. #[derive(Debug, Deserialize, Serialize, Clone)] pub struct ProviderEntry { #[serde(default)] @@ -23,10 +18,6 @@ pub struct ProviderEntry { pub configured: bool, } -// --------------------------------------------------------------------------- -// Read helpers -// --------------------------------------------------------------------------- - fn parse_providers_map(raw: Mapping) -> IndexMap { let mut map = IndexMap::with_capacity(raw.len()); for (k, v) in raw { @@ -53,16 +44,10 @@ fn get_providers_map(config: &Config) -> IndexMap { parse_providers_map(raw) } -/// Retrieve the [`ProviderEntry`] for a named provider, if it exists. pub fn get_provider_entry(config: &Config, name: &str) -> Option { get_providers_map(config).get(name).cloned() } -// --------------------------------------------------------------------------- -// Write helpers -// --------------------------------------------------------------------------- - -/// Persist a [`ProviderEntry`] under `providers.{name}`. pub fn set_provider_entry( config: &Config, name: &str, @@ -77,45 +62,20 @@ pub fn set_provider_entry( }) } -// --------------------------------------------------------------------------- -// Active-provider accessors -// --------------------------------------------------------------------------- - -/// Return the currently active provider name. -/// -/// Resolution order: -/// 1. `GOOSE_PROVIDER` environment variable (uppercase check performed by -/// `get_param`) -/// 2. `active_provider` key in config.yaml -/// 3. Legacy flat `GOOSE_PROVIDER` key in config.yaml (backward compat) pub fn get_active_provider(config: &Config) -> Option { - // Env var takes precedence (get_param checks env automatically) if let Ok(val) = env::var("GOOSE_PROVIDER") { return Some(val); } - - // New structured key if let Ok(val) = config.get_param::(ACTIVE_PROVIDER_KEY) { return Some(val); } - - // Legacy flat key fallback config.get_param::("GOOSE_PROVIDER").ok() } -/// Return the model for the currently active provider. -/// -/// Resolution order: -/// 1. `GOOSE_MODEL` environment variable -/// 2. Model recorded in the active provider's entry (`providers.{name}.model`) -/// 3. Legacy flat `GOOSE_MODEL` key in config.yaml pub fn get_active_model(config: &Config) -> Option { - // Env var takes precedence if let Ok(val) = env::var("GOOSE_MODEL") { return Some(val); } - - // Try provider entry model if let Some(provider_name) = get_active_provider(config) { if let Some(entry) = get_provider_entry(config, &provider_name) { if !entry.model.is_empty() { @@ -123,17 +83,9 @@ pub fn get_active_model(config: &Config) -> Option { } } } - - // Legacy flat key fallback config.get_param::("GOOSE_MODEL").ok() } -/// Set the active provider and update its entry in the `providers` block. -/// -/// This writes: -/// - `active_provider: {name}` at the top level -/// - `providers.{name}` with `configured: true`, `enabled: true`, and the -/// supplied model. pub fn set_active_provider(config: &Config, name: &str, model: &str) -> Result<(), ConfigError> { config.set_param(ACTIVE_PROVIDER_KEY, name)?; let entry = ProviderEntry { @@ -144,6 +96,16 @@ pub fn set_active_provider(config: &Config, name: &str, model: &str) -> Result<( set_provider_entry(config, name, &entry) } +pub fn clear_active_provider(config: &Config) -> Result<(), ConfigError> { + for key in [ACTIVE_PROVIDER_KEY, "GOOSE_PROVIDER", "GOOSE_MODEL"] { + match config.delete(key) { + Ok(()) | Err(ConfigError::NotFound(_)) => {} + Err(e) => return Err(e), + } + } + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -171,12 +133,6 @@ mod tests { assert!(loaded.configured); } - #[test] - fn test_get_provider_entry_missing() { - let config = new_test_config(); - assert!(get_provider_entry(&config, "nonexistent").is_none()); - } - #[test] fn test_set_active_provider_writes_structured_keys() { let config = new_test_config(); @@ -192,28 +148,28 @@ mod tests { } #[test] - fn test_get_active_provider_from_new_key() { + fn test_clear_active_provider_preserves_provider_entries() { let config = new_test_config(); - config.set_param(ACTIVE_PROVIDER_KEY, "openai").unwrap(); + set_active_provider(&config, "openai", "gpt-4o").unwrap(); - let result = get_active_provider(&config); - assert_eq!(result, Some("openai".to_string())); + clear_active_provider(&config).unwrap(); + + assert!(get_active_provider(&config).is_none()); + let entry = get_provider_entry(&config, "openai").unwrap(); + assert_eq!(entry.model, "gpt-4o"); + assert!(entry.configured); } #[test] - fn test_get_active_provider_falls_back_to_legacy() { + fn test_clear_active_provider_removes_legacy_keys() { let config = new_test_config(); config.set_param("GOOSE_PROVIDER", "anthropic").unwrap(); + config.set_param("GOOSE_MODEL", "claude").unwrap(); - let result = get_active_provider(&config); - assert_eq!(result, Some("anthropic".to_string())); - } + clear_active_provider(&config).unwrap(); - #[test] - fn test_get_active_provider_none_when_empty() { - let config = new_test_config(); - let result = get_active_provider(&config); - assert_eq!(result, None); + assert!(get_active_provider(&config).is_none()); + assert!(get_active_model(&config).is_none()); } #[test] @@ -225,25 +181,12 @@ mod tests { assert_eq!(result, Some("gpt-4o".to_string())); } - #[test] - fn test_get_active_model_falls_back_to_legacy() { - let config = new_test_config(); - // Only set the legacy key, no providers block - config.set_param("GOOSE_MODEL", "gpt-3.5-turbo").unwrap(); - - let result = get_active_model(&config); - assert_eq!(result, Some("gpt-3.5-turbo".to_string())); - } - #[test] fn test_multiple_providers_preserved() { let config = new_test_config(); - - // Set up two providers set_active_provider(&config, "openai", "gpt-4o").unwrap(); set_active_provider(&config, "anthropic", "claude-3-opus").unwrap(); - // Both entries should exist let openai = get_provider_entry(&config, "openai").unwrap(); assert_eq!(openai.model, "gpt-4o"); assert!(openai.configured); @@ -252,8 +195,6 @@ mod tests { assert_eq!(anthropic.model, "claude-3-opus"); assert!(anthropic.configured); - // Active provider should be the last one set - let active = get_active_provider(&config); - assert_eq!(active, Some("anthropic".to_string())); + assert_eq!(get_active_provider(&config), Some("anthropic".to_string())); } } diff --git a/crates/goose/src/providers/mod.rs b/crates/goose/src/providers/mod.rs index 06ba551ed640..57a0c59ca666 100644 --- a/crates/goose/src/providers/mod.rs +++ b/crates/goose/src/providers/mod.rs @@ -62,6 +62,7 @@ pub mod openai_compatible { pub mod openrouter; pub mod pi_acp; pub mod provider_registry; +pub mod provider_secrets; pub mod provider_test; mod retry { pub use goose_providers::retry::*; diff --git a/crates/goose/src/providers/provider_secrets.rs b/crates/goose/src/providers/provider_secrets.rs new file mode 100644 index 000000000000..97859b46180b --- /dev/null +++ b/crates/goose/src/providers/provider_secrets.rs @@ -0,0 +1,651 @@ +use std::collections::{HashMap, HashSet}; +use std::path::Path; + +use chrono::{DateTime, TimeZone, Utc}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use utoipa::ToSchema; + +use crate::config::paths::Paths; +use crate::config::{Config, ConfigError}; +use crate::providers::base::{ProviderMetadata, ProviderType}; +use crate::providers::huggingface_auth; + +pub const SECRET_STORE_ID_PREFIX: &str = "secret_store:"; +pub const PROVIDER_CACHE_ID_PREFIX: &str = "provider_cache:"; + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ProviderSecretStorage { + SecretStore, + ProviderCache, +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ProviderSecretStatus { + Valid, + Expired, + Unknown, +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct ProviderSecret { + pub id: String, + pub provider: String, + pub provider_display_name: String, + pub name: String, + pub storage: ProviderSecretStorage, + pub expires_at: Option>, + pub status: ProviderSecretStatus, + pub configured: bool, + pub has_secret: bool, + pub can_delete: bool, + pub can_configure: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub configure_provider: Option, +} + +#[derive(Debug, thiserror::Error)] +pub enum DeleteProviderSecretError { + #[error("Invalid provider secret id: '{0}'")] + InvalidId(String), + #[error(transparent)] + Config(#[from] ConfigError), + #[error(transparent)] + Other(#[from] anyhow::Error), +} + +fn provider_secret_status(expires_at: Option>) -> ProviderSecretStatus { + match expires_at { + Some(expires_at) if expires_at <= Utc::now() => ProviderSecretStatus::Expired, + Some(_) => ProviderSecretStatus::Valid, + None => ProviderSecretStatus::Unknown, + } +} + +fn parse_expiry_value(value: &Value) -> Option> { + match value { + Value::String(value) => DateTime::parse_from_rfc3339(value) + .ok() + .map(|dt| dt.with_timezone(&Utc)), + Value::Number(value) => value + .as_i64() + .and_then(|timestamp| Utc.timestamp_opt(timestamp, 0).single()), + _ => None, + } +} + +fn find_expires_at(value: &Value) -> Option> { + match value { + Value::Object(map) => { + if map + .get("refresh_token") + .and_then(Value::as_str) + .is_some_and(|token| !token.is_empty()) + { + return None; + } + if let Some(expires_at) = map.get("expires_at").and_then(parse_expiry_value) { + return Some(expires_at); + } + if let Some(expires_at) = map.get("expires_on").and_then(parse_expiry_value) { + return Some(expires_at); + } + map.values().find_map(find_expires_at) + } + Value::Array(values) => values.iter().find_map(find_expires_at), + _ => None, + } +} + +#[derive(Clone, Copy)] +struct ProviderCacheSecretDefinition { + provider: &'static str, + name: &'static str, + path: &'static str, + is_directory: bool, +} + +const PROVIDER_CACHE_SECRET_DEFINITIONS: &[ProviderCacheSecretDefinition] = &[ + ProviderCacheSecretDefinition { + provider: "gemini_oauth", + name: "OAuth token", + path: "gemini_oauth/tokens.json", + is_directory: false, + }, + ProviderCacheSecretDefinition { + provider: "chatgpt_codex", + name: "OAuth token", + path: "chatgpt_codex/tokens.json", + is_directory: false, + }, + ProviderCacheSecretDefinition { + provider: "kimi_code", + name: "OAuth token", + path: "kimicode/token.json", + is_directory: false, + }, + ProviderCacheSecretDefinition { + provider: "github_copilot", + name: "OAuth token", + path: "githubcopilot", + is_directory: true, + }, + ProviderCacheSecretDefinition { + provider: "xai_oauth", + name: "OAuth token", + path: "xai_oauth/tokens.json", + is_directory: false, + }, + ProviderCacheSecretDefinition { + provider: "databricks", + name: "OAuth token", + path: "databricks/oauth", + is_directory: true, + }, + ProviderCacheSecretDefinition { + provider: "databricks_v2", + name: "OAuth token", + path: "databricks/oauth", + is_directory: true, + }, +]; + +fn provider_cache_definitions_for_display() -> Vec { + let mut seen_paths = HashSet::new(); + PROVIDER_CACHE_SECRET_DEFINITIONS + .iter() + .copied() + .filter(|definition| seen_paths.insert(definition.path)) + .collect() +} + +fn provider_cache_definition(provider: &str) -> Option { + PROVIDER_CACHE_SECRET_DEFINITIONS + .iter() + .copied() + .find(|definition| definition.provider == provider) +} + +fn provider_cache_providers_sharing_cache(provider: &str) -> Vec<&'static str> { + let Some(definition) = provider_cache_definition(provider) else { + return Vec::new(); + }; + + PROVIDER_CACHE_SECRET_DEFINITIONS + .iter() + .filter(|other| other.path == definition.path) + .map(|definition| definition.provider) + .collect() +} + +fn read_json_file(path: &Path) -> Option { + std::fs::read_to_string(path) + .ok() + .and_then(|contents| serde_json::from_str(&contents).ok()) +} + +fn collect_json_expiries(path: &Path, is_directory: bool) -> Vec> { + if !is_directory { + return read_json_file(path) + .and_then(|value| find_expires_at(&value)) + .into_iter() + .collect(); + } + + let mut expiries = Vec::new(); + let mut stack = vec![path.to_path_buf()]; + + while let Some(current) = stack.pop() { + let Ok(entries) = std::fs::read_dir(current) else { + continue; + }; + + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + stack.push(path); + continue; + } + if path.extension().and_then(|ext| ext.to_str()) != Some("json") { + continue; + } + if let Some(expires_at) = + read_json_file(&path).and_then(|value| find_expires_at(&value)) + { + expiries.push(expires_at); + } + } + } + + expiries +} + +fn provider_cache_exists(path: &Path, is_directory: bool) -> bool { + if !is_directory { + return path.is_file(); + } + + let Ok(entries) = std::fs::read_dir(path) else { + return false; + }; + + entries.flatten().any(|entry| { + let path = entry.path(); + path.is_file() || provider_cache_exists(&path, true) + }) +} + +fn provider_cache_expiry(definition: ProviderCacheSecretDefinition) -> Option> { + let path = Paths::in_config_dir(definition.path); + collect_json_expiries(&path, definition.is_directory) + .into_iter() + .min() +} + +fn build_provider_cache_secret( + definition: ProviderCacheSecretDefinition, + display_names: &HashMap, +) -> Option { + let path = Paths::in_config_dir(definition.path); + if !provider_cache_exists(&path, definition.is_directory) { + return None; + } + + let expires_at = provider_cache_expiry(definition); + Some(ProviderSecret { + id: format!("{}{}", PROVIDER_CACHE_ID_PREFIX, definition.provider), + provider: definition.provider.to_string(), + provider_display_name: display_names + .get(definition.provider) + .cloned() + .unwrap_or_else(|| definition.provider.to_string()), + name: definition.name.to_string(), + storage: ProviderSecretStorage::ProviderCache, + expires_at, + status: provider_secret_status(expires_at), + configured: true, + has_secret: true, + can_delete: true, + can_configure: false, + configure_provider: None, + }) +} + +fn build_huggingface_oauth_secret( + token: Option, +) -> ProviderSecret { + let expires_at = token.as_ref().and_then(|token| token.expires_at); + let has_secret = token.is_some(); + + ProviderSecret { + id: format!( + "{}{}", + PROVIDER_CACHE_ID_PREFIX, + huggingface_auth::HUGGINGFACE_PROVIDER_NAME + ), + provider: huggingface_auth::HUGGINGFACE_PROVIDER_NAME.to_string(), + provider_display_name: huggingface_auth::HUGGINGFACE_DISPLAY_NAME.to_string(), + name: huggingface_auth::HUGGINGFACE_OAUTH_TOKEN_NAME.to_string(), + storage: ProviderSecretStorage::ProviderCache, + expires_at, + status: provider_secret_status(expires_at), + configured: has_secret, + has_secret, + can_delete: has_secret, + can_configure: true, + configure_provider: Some(huggingface_auth::HUGGINGFACE_PROVIDER_NAME.to_string()), + } +} + +fn build_secret_store_secrets( + stored_secrets: &HashMap, + providers: &[(ProviderMetadata, ProviderType)], +) -> Vec { + let mut secrets = Vec::new(); + + for (metadata, _) in providers { + for config_key in metadata.config_keys.iter().filter(|key| key.secret) { + if !stored_secrets.contains_key(&config_key.name) { + continue; + } + secrets.push(ProviderSecret { + id: format!( + "{}{}:{}", + SECRET_STORE_ID_PREFIX, metadata.name, config_key.name + ), + provider: metadata.name.clone(), + provider_display_name: metadata.display_name.clone(), + name: config_key.name.clone(), + storage: ProviderSecretStorage::SecretStore, + expires_at: None, + status: ProviderSecretStatus::Unknown, + configured: true, + has_secret: true, + can_delete: true, + can_configure: false, + configure_provider: None, + }); + } + } + + secrets +} + +fn is_known_provider_secret( + providers: &[(ProviderMetadata, ProviderType)], + provider: &str, + key: &str, +) -> bool { + providers + .iter() + .filter(|(metadata, _)| metadata.name == provider) + .flat_map(|(metadata, _)| metadata.config_keys.iter()) + .any(|config_key| config_key.secret && config_key.name == key) +} + +fn unconfigure_provider(config: &Config, provider_name: &str) -> Result<(), ConfigError> { + if let Some(mut entry) = crate::config::get_provider_entry(config, provider_name) { + entry.configured = false; + crate::config::set_provider_entry(config, provider_name, &entry)?; + } + + let configured_marker = format!("{}_configured", provider_name); + match config.delete(&configured_marker) { + Ok(()) | Err(ConfigError::NotFound(_)) => Ok(()), + Err(e) => Err(e), + } +} + +fn parse_secret_store_id(id: &str) -> Option<(&str, &str)> { + let rest = id.strip_prefix(SECRET_STORE_ID_PREFIX)?; + rest.split_once(':') +} + +fn parse_provider_cache_id(id: &str) -> Option<&str> { + id.strip_prefix(PROVIDER_CACHE_ID_PREFIX) +} + +fn is_valid_provider_name(provider_name: &str) -> bool { + !provider_name.is_empty() + && provider_name + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') +} + +fn should_unconfigure_after_secret_delete( + provider: &str, + key: &str, + has_usable_huggingface_oauth_token: impl FnOnce() -> bool, +) -> bool { + provider == huggingface_auth::HUGGINGFACE_PROVIDER_NAME + && key == huggingface_auth::HUGGINGFACE_TOKEN_SECRET_KEY + && !has_usable_huggingface_oauth_token() +} + +pub async fn list_provider_secrets() -> Result, ConfigError> { + let config = Config::global(); + let stored_secrets = config.all_secrets()?; + let providers = crate::providers::providers().await; + let display_names: HashMap = providers + .iter() + .map(|(metadata, _)| (metadata.name.clone(), metadata.display_name.clone())) + .collect(); + + let mut secrets = build_secret_store_secrets(&stored_secrets, &providers); + + for definition in provider_cache_definitions_for_display() { + if let Some(secret) = build_provider_cache_secret(definition, &display_names) { + if !secrets.iter().any(|existing| existing.id == secret.id) { + secrets.push(secret); + } + } + } + + let huggingface_secret = build_huggingface_oauth_secret(huggingface_auth::load_oauth_token()); + if let Some(existing) = secrets + .iter_mut() + .find(|existing| existing.id == huggingface_secret.id) + { + *existing = huggingface_secret; + } else { + secrets.push(huggingface_secret); + } + + secrets.sort_by(|a, b| { + a.provider_display_name + .cmp(&b.provider_display_name) + .then_with(|| a.name.cmp(&b.name)) + }); + + Ok(secrets) +} + +pub async fn delete_provider_secret(id: &str) -> Result<(), DeleteProviderSecretError> { + let config = Config::global(); + + if let Some((provider, key)) = parse_secret_store_id(id) { + let providers = crate::providers::providers().await; + if !is_known_provider_secret(&providers, provider, key) { + return Err(DeleteProviderSecretError::InvalidId(id.to_string())); + } + + config.delete_secret(key)?; + if should_unconfigure_after_secret_delete(provider, key, || { + huggingface_auth::has_configured_token().unwrap_or(false) + }) { + unconfigure_provider(config, provider)?; + } + return Ok(()); + } + + if let Some(provider) = parse_provider_cache_id(id) { + if provider == huggingface_auth::HUGGINGFACE_PROVIDER_NAME { + huggingface_auth::clear_oauth_token()?; + unconfigure_provider(config, provider)?; + return Ok(()); + } + + if !is_valid_provider_name(provider) || provider_cache_definition(provider).is_none() { + return Err(DeleteProviderSecretError::InvalidId(id.to_string())); + } + crate::providers::cleanup_provider(provider).await?; + for shared_provider in provider_cache_providers_sharing_cache(provider) { + unconfigure_provider(config, shared_provider)?; + } + return Ok(()); + } + + Err(DeleteProviderSecretError::InvalidId(id.to_string())) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::ProviderEntry; + use crate::providers::base::ConfigKey; + use serde_json::json; + + fn new_test_config() -> Config { + let unique = format!( + "goose-provider-secrets-test-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + ); + let config_path = std::env::temp_dir().join(format!("{unique}-config.yaml")); + let secrets_path = std::env::temp_dir().join(format!("{unique}-secrets.yaml")); + Config::new_with_file_secrets(config_path, secrets_path).unwrap() + } + + #[test] + fn secret_store_listing_only_includes_provider_secret_keys() { + let metadata = ProviderMetadata::new( + "openai", + "OpenAI", + "OpenAI provider", + "gpt-4o", + vec![], + "https://example.com", + vec![ + ConfigKey::new("OPENAI_API_KEY", true, true, None, true), + ConfigKey::new("OPENAI_HOST", false, false, None, false), + ], + ); + let providers = vec![(metadata, ProviderType::Builtin)]; + let stored_secrets = HashMap::from([ + ( + "OPENAI_API_KEY".to_string(), + Value::String("secret-value".to_string()), + ), + ( + "UNRELATED_SECRET".to_string(), + Value::String("other-secret".to_string()), + ), + ( + "OPENAI_HOST".to_string(), + Value::String("https://api.openai.com".to_string()), + ), + ]); + + let secrets = build_secret_store_secrets(&stored_secrets, &providers); + + assert_eq!(secrets.len(), 1); + assert_eq!(secrets[0].id, "secret_store:openai:OPENAI_API_KEY"); + assert_eq!(secrets[0].provider_display_name, "OpenAI"); + assert_eq!(secrets[0].name, "OPENAI_API_KEY"); + assert_eq!(secrets[0].storage, ProviderSecretStorage::SecretStore); + assert_eq!(secrets[0].status, ProviderSecretStatus::Unknown); + } + + #[test] + fn provider_secret_delete_validation_requires_provider_secret_key() { + let metadata = ProviderMetadata::new( + "openai", + "OpenAI", + "OpenAI provider", + "gpt-4o", + vec![], + "https://example.com", + vec![ + ConfigKey::new("OPENAI_API_KEY", true, true, None, true), + ConfigKey::new("OPENAI_HOST", false, false, None, false), + ], + ); + let providers = vec![(metadata, ProviderType::Builtin)]; + + assert!(is_known_provider_secret( + &providers, + "openai", + "OPENAI_API_KEY" + )); + assert!(!is_known_provider_secret( + &providers, + "openai", + "OPENAI_HOST" + )); + assert!(!is_known_provider_secret( + &providers, + "openai", + "UNRELATED_SECRET" + )); + assert!(!is_known_provider_secret( + &providers, + "anthropic", + "OPENAI_API_KEY" + )); + } + + #[test] + fn expiry_extraction_handles_nested_rfc3339_values() { + let expires_at = Utc::now() + chrono::Duration::hours(1); + let value = json!({ + "project_id": "project", + "token": { + "access_token": "secret", + "expires_at": expires_at.to_rfc3339(), + } + }); + + let parsed = find_expires_at(&value).expect("expected expiry"); + + assert_eq!(parsed.timestamp(), expires_at.timestamp()); + assert_eq!( + provider_secret_status(Some(parsed)), + ProviderSecretStatus::Valid + ); + } + + #[test] + fn expiry_extraction_ignores_refreshable_access_tokens() { + let expires_at = Utc::now() - chrono::Duration::hours(1); + let value = json!({ + "access_token": "access", + "refresh_token": "refresh", + "expires_at": expires_at.to_rfc3339(), + }); + + assert_eq!(find_expires_at(&value), None); + } + + #[test] + fn expiry_extraction_handles_expired_unix_timestamps() { + let value = json!({ + "info": { + "expires_at": 1 + } + }); + + let parsed = find_expires_at(&value).expect("expected expiry"); + + assert_eq!(parsed.timestamp(), 1); + assert_eq!( + provider_secret_status(Some(parsed)), + ProviderSecretStatus::Expired + ); + } + + #[test] + fn unconfigure_provider_clears_structured_entry() { + let config = new_test_config(); + crate::config::set_provider_entry( + &config, + "huggingface", + &ProviderEntry { + enabled: true, + model: "Qwen/Qwen3-Coder-480B-A35B-Instruct".to_string(), + configured: true, + }, + ) + .unwrap(); + + unconfigure_provider(&config, "huggingface").unwrap(); + + let entry = crate::config::get_provider_entry(&config, "huggingface").unwrap(); + assert!(entry.enabled); + assert_eq!(entry.model, "Qwen/Qwen3-Coder-480B-A35B-Instruct"); + assert!(!entry.configured); + } + + #[test] + fn unconfigure_provider_deletes_legacy_configured_marker() { + let config = new_test_config(); + config.set_param("huggingface_configured", true).unwrap(); + + unconfigure_provider(&config, "huggingface").unwrap(); + + assert!(config.get_param::("huggingface_configured").is_err()); + } + + #[test] + fn deleting_huggingface_token_unconfigures_without_oauth() { + assert!(should_unconfigure_after_secret_delete( + "huggingface", + "HF_TOKEN", + || false + )); + } +} diff --git a/crates/goose/tests/acp_custom_requests_test.rs b/crates/goose/tests/acp_custom_requests_test.rs index c69ea6dd0425..cf5ec0569cde 100644 --- a/crates/goose/tests/acp_custom_requests_test.rs +++ b/crates/goose/tests/acp_custom_requests_test.rs @@ -441,6 +441,100 @@ fn test_custom_get_available_extensions() { }); } +#[test] +#[serial] +fn test_custom_prompt_methods() { + let _guard = env_lock::lock_env([("EXTENSIONS", None::<&str>)]); + write_acp_global_config(DEFAULT_ACP_TEST_CONFIG); + + run_test(async move { + let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await; + let conn = AcpServerConnection::new(TestConnectionConfig::default(), openai).await; + + let list_response = send_custom( + conn.cx(), + "_goose/unstable/config/prompts/list", + serde_json::json!({}), + ) + .await + .expect("list prompts should succeed"); + let prompts = list_response["prompts"] + .as_array() + .expect("prompts should be an array"); + assert!( + prompts.iter().any(|prompt| prompt["name"] == "system.md"), + "system.md should be listed" + ); + + let get_response = send_custom( + conn.cx(), + "_goose/unstable/config/prompts/get", + serde_json::json!({ "name": "system.md" }), + ) + .await + .expect("get prompt should succeed"); + assert_eq!(get_response["name"], "system.md"); + assert!(get_response["content"] + .as_str() + .is_some_and(|s| !s.is_empty())); + assert_eq!(get_response["isCustomized"], false); + + let content = "custom acp system prompt"; + let save_response = send_custom( + conn.cx(), + "_goose/unstable/config/prompts/save", + serde_json::json!({ "name": "system.md", "content": content }), + ) + .await + .expect("save prompt should succeed"); + assert_eq!(save_response["message"], "Saved prompt: system.md"); + + let get_response = send_custom( + conn.cx(), + "_goose/unstable/config/prompts/get", + serde_json::json!({ "name": "system.md" }), + ) + .await + .expect("get saved prompt should succeed"); + assert_eq!(get_response["content"], content); + assert_eq!(get_response["isCustomized"], true); + + let reset_response = send_custom( + conn.cx(), + "_goose/unstable/config/prompts/reset", + serde_json::json!({ "name": "system.md" }), + ) + .await + .expect("reset prompt should succeed"); + assert_eq!( + reset_response["message"], + "Reset prompt to default: system.md" + ); + + let get_response = send_custom( + conn.cx(), + "_goose/unstable/config/prompts/get", + serde_json::json!({ "name": "system.md" }), + ) + .await + .expect("get reset prompt should succeed"); + assert_eq!(get_response["isCustomized"], false); + assert_ne!(get_response["content"], content); + + let missing = send_custom( + conn.cx(), + "_goose/unstable/config/prompts/get", + serde_json::json!({ "name": "missing.md" }), + ) + .await + .expect_err("unknown prompt should fail"); + assert_eq!( + missing.code, + agent_client_protocol::ErrorCode::InvalidParams + ); + }); +} + #[test] #[serial] fn test_steer_session_adds_input_to_active_prompt() { diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index c49b6c02ea46..d2229df0ed3f 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -791,39 +791,6 @@ } } }, - "/config/permissions": { - "post": { - "tags": [ - "super::routes::config_management" - ], - "operationId": "upsert_permissions", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpsertPermissionsQuery" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Permission update completed", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } - }, - "400": { - "description": "Invalid request" - } - } - } - }, "/config/prompts": { "get": { "tags": [ @@ -1240,33 +1207,6 @@ } } }, - "/config/providers/{name}/oauth": { - "post": { - "tags": [ - "super::routes::config_management" - ], - "operationId": "configure_provider_oauth", - "parameters": [ - { - "name": "name", - "in": "path", - "description": "Provider name", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OAuth configuration completed" - }, - "400": { - "description": "OAuth configuration failed" - } - } - } - }, "/config/read": { "post": { "tags": [ @@ -3325,27 +3265,6 @@ } } } - }, - "/tunnel/status": { - "get": { - "tags": [ - "super::routes::tunnel" - ], - "summary": "Get tunnel info", - "operationId": "get_tunnel_status", - "responses": { - "200": { - "description": "Tunnel info", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TunnelInfo" - } - } - } - } - } - } } }, "components": { @@ -8363,21 +8282,6 @@ } } }, - "ToolPermission": { - "type": "object", - "required": [ - "tool_name", - "permission" - ], - "properties": { - "permission": { - "$ref": "#/components/schemas/PermissionLevel" - }, - "tool_name": { - "type": "string" - } - } - }, "ToolRequest": { "type": "object", "required": [ @@ -8450,39 +8354,6 @@ } } }, - "TunnelInfo": { - "type": "object", - "required": [ - "state", - "url", - "hostname", - "secret" - ], - "properties": { - "hostname": { - "type": "string" - }, - "secret": { - "type": "string" - }, - "state": { - "$ref": "#/components/schemas/TunnelState" - }, - "url": { - "type": "string" - } - } - }, - "TunnelState": { - "type": "string", - "enum": [ - "idle", - "starting", - "running", - "error", - "disabled" - ] - }, "UiMetadata": { "type": "object", "description": "UI-specific metadata for MCP resources", @@ -8702,20 +8573,6 @@ "value": {} } }, - "UpsertPermissionsQuery": { - "type": "object", - "required": [ - "tool_permissions" - ], - "properties": { - "tool_permissions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ToolPermission" - } - } - } - }, "Usage": { "type": "object", "description": "`input_tokens` is the total input including cache read/write tokens;\nthe cache fields are breakdown subsets of it. Parsers for providers\nthat report cache tokens separately from input (e.g. Anthropic,\nBedrock) must fold them into `input_tokens`.", diff --git a/ui/desktop/src/App.test.tsx b/ui/desktop/src/App.test.tsx index 501e34fd9449..d9c4e5d5fae8 100644 --- a/ui/desktop/src/App.test.tsx +++ b/ui/desktop/src/App.test.tsx @@ -46,7 +46,6 @@ vi.mock('./api', () => { return { initConfig: vi.fn().mockResolvedValue(undefined), - readAllConfig: vi.fn().mockResolvedValue(undefined), backupConfig: vi.fn().mockResolvedValue(undefined), recoverConfig: vi.fn().mockResolvedValue(undefined), validateConfig: vi.fn().mockResolvedValue(undefined), @@ -63,6 +62,15 @@ vi.mock('./sessions', () => ({ createSession: vi.fn(), })); +// Mock the ACP providers module used by OnboardingGuard so it doesn't try to +// open a real ACP client connection during tests. Returning null defaults +// keeps the app in the "brand new" (no provider configured) onboarding state. +vi.mock('./acp/providers', () => ({ + acpReadDefaults: vi.fn().mockResolvedValue({ providerId: null, modelId: null }), + acpSaveDefaults: vi.fn().mockResolvedValue(undefined), + acpListProviderDetails: vi.fn().mockResolvedValue([]), +})); + // Mock the ConfigContext module vi.mock('./components/ConfigContext', () => ({ useConfig: () => ({ diff --git a/ui/desktop/src/acp/__tests__/chatSessionController.test.ts b/ui/desktop/src/acp/__tests__/chatSessionController.test.ts index be0f0b9a720d..9f9ad0c7fa23 100644 --- a/ui/desktop/src/acp/__tests__/chatSessionController.test.ts +++ b/ui/desktop/src/acp/__tests__/chatSessionController.test.ts @@ -35,6 +35,8 @@ vi.mock('../chatSessionStore', () => ({ clearActivePromptAttempt: vi.fn(), startPromptCancellation: vi.fn(), clearPromptCancellation: vi.fn(), + restorePromptCancellation: vi.fn(), + waitForPromptCancellation: vi.fn(), setChatState: vi.fn(), setSessionMetadata: vi.fn(), setSessionLoadError: vi.fn(), @@ -113,6 +115,24 @@ function snapshotWithActivePrompt(activePromptAttemptId: string | null): AcpChat }; } +function pendingToolPermissionMessage(): Message & { id: string } { + return { + id: 'permission-message-1', + role: 'assistant', + created: 124, + content: [ + { + type: 'toolConfirmationRequest', + id: 'tool-call-1', + toolName: 'developer__shell', + arguments: {}, + prompt: null, + }, + ], + metadata: { userVisible: true, agentVisible: true }, + }; +} + describe('acpChatSessionController.loadSession', () => { beforeEach(() => { vi.clearAllMocks(); @@ -219,7 +239,9 @@ describe('acpChatSessionController.updateMessage', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(acpTruncateSessionConversation).mockResolvedValue(undefined as never); + vi.mocked(acpPromptSession).mockResolvedValue({ stopReason: 'end_turn' } as never); vi.mocked(acpChatSessionStore.getSnapshot).mockReturnValue(snapshotWithActivePrompt(null)); + vi.mocked(acpChatSessionActions.waitForPromptCancellation).mockResolvedValue(undefined); }); it('rejects edits before truncating while cancellation is pending', async () => { @@ -249,7 +271,7 @@ describe('acpChatSessionController.updateMessage', () => { expect(acpPromptSession).not.toHaveBeenCalled(); }); - it('rejects edits before truncating while a prompt is active', async () => { + it('ignores edits before truncating while a prompt is active', async () => { vi.mocked(acpChatSessionStore.getSnapshot).mockReturnValue( snapshotWithActivePrompt('attempt-1') ); @@ -264,7 +286,7 @@ describe('acpChatSessionController.updateMessage', () => { getCurrentSnapshot: () => currentSnapshot, onFinish: vi.fn(), }) - ).rejects.toThrow('Cannot update message while prompt is active'); + ).resolves.toBeUndefined(); expect(acpChatSessionActions.setChatState).not.toHaveBeenCalledWith( SESSION_ID, @@ -274,4 +296,66 @@ describe('acpChatSessionController.updateMessage', () => { expect(acpChatSessionActions.setMessages).not.toHaveBeenCalled(); expect(acpPromptSession).not.toHaveBeenCalled(); }); + + it('waits for pending tool permission cancellation before truncating and rerunning', async () => { + const existingMessage = userMessage(); + const permissionMessage = pendingToolPermissionMessage(); + const activeSnapshot: AcpChatSessionSnapshot = { + ...snapshotWithActivePrompt('attempt-1'), + chatState: ChatState.WaitingForUserInput, + messages: [existingMessage, permissionMessage], + }; + let storedSnapshot = activeSnapshot; + vi.mocked(acpChatSessionStore.getSnapshot).mockImplementation(() => storedSnapshot); + vi.mocked(acpChatSessionActions.startPromptCancellation).mockReturnValue({ + ...activeSnapshot, + activePromptAttemptId: null, + pendingCancelPromptAttemptId: 'attempt-1', + }); + vi.mocked(acpCancelPrompt).mockResolvedValue(undefined); + + let resolvePromptCancellation: () => void; + const promptCancellationSettled = new Promise((resolve) => { + resolvePromptCancellation = resolve; + }); + vi.mocked(acpChatSessionActions.waitForPromptCancellation).mockReturnValue( + promptCancellationSettled + ); + + const updatePromise = acpChatSessionController.updateMessage( + SESSION_ID, + existingMessage.id, + 'Updated', + 'edit', + { + getCurrentSnapshot: () => activeSnapshot, + onFinish: vi.fn(), + } + ); + + await Promise.resolve(); + await Promise.resolve(); + + expect(acpCancelPrompt).toHaveBeenCalledWith(SESSION_ID); + expect(acpChatSessionActions.waitForPromptCancellation).toHaveBeenCalledWith( + SESSION_ID, + 'attempt-1' + ); + expect(acpTruncateSessionConversation).not.toHaveBeenCalled(); + expect(acpPromptSession).not.toHaveBeenCalled(); + + storedSnapshot = { + ...snapshotWithActivePrompt(null), + messages: [existingMessage, permissionMessage], + }; + resolvePromptCancellation!(); + await updatePromise; + + expect(acpTruncateSessionConversation).toHaveBeenCalledWith(SESSION_ID, existingMessage.created); + expect(acpPromptSession).toHaveBeenCalled(); + expect(acpChatSessionActions.clearPromptCancellation).not.toHaveBeenCalledWith( + SESSION_ID, + 'attempt-1' + ); + }); }); diff --git a/ui/desktop/src/acp/__tests__/chatSessionStore.test.ts b/ui/desktop/src/acp/__tests__/chatSessionStore.test.ts index 23834d0b9189..68502de09d1a 100644 --- a/ui/desktop/src/acp/__tests__/chatSessionStore.test.ts +++ b/ui/desktop/src/acp/__tests__/chatSessionStore.test.ts @@ -8,7 +8,9 @@ import { afterEach, describe, expect, it } from 'vitest'; import type { Message, Session } from '../../api'; import { ChatState } from '../../types/chatState'; import { + acpElicitationUserInputRequestId, acpChatSessionActions, + acpPermissionUserInputRequestId, acpChatSessionStore, useAcpChatSessionSnapshot, } from '../chatSessionStore'; @@ -294,6 +296,58 @@ describe('acpChatSessionStore', () => { expect(clearedSnapshot?.pendingCancelPromptAttemptId).toBeNull(); }); + it('restores pending user input tracking when prompt cancellation is restored', () => { + const currentSessionId = sessionId('session-1'); + + acpChatSessionActions.startPromptAttempt(currentSessionId, 'attempt-1'); + acpChatSessionActions.applyPermissionRequest(permissionRequest(currentSessionId, 'tool-1')); + acpChatSessionActions.applyElicitationRequest(elicitationRequest(currentSessionId)); + acpChatSessionActions.startPromptCancellation(currentSessionId, 'attempt-1'); + + const restoredSnapshot = acpChatSessionActions.restorePromptCancellation( + currentSessionId, + 'attempt-1' + ); + + expect(restoredSnapshot?.chatState).toBe(ChatState.WaitingForUserInput); + + const afterPermission = acpChatSessionActions.resolveUserInputRequest( + currentSessionId, + acpPermissionUserInputRequestId('tool-1') + ); + + expect(afterPermission?.chatState).toBe(ChatState.WaitingForUserInput); + + const afterElicitation = acpChatSessionActions.resolveUserInputRequest( + currentSessionId, + acpElicitationUserInputRequestId('acp_elicitation_1') + ); + + expect(afterElicitation?.chatState).toBe(ChatState.Streaming); + }); + + it('waits for prompt cancellation to clear', async () => { + const currentSessionId = sessionId('session-1'); + + acpChatSessionActions.startPromptAttempt(currentSessionId, 'attempt-1'); + acpChatSessionActions.startPromptCancellation(currentSessionId, 'attempt-1'); + + let didResolve = false; + const waitPromise = acpChatSessionActions + .waitForPromptCancellation(currentSessionId, 'attempt-1') + .then(() => { + didResolve = true; + }); + + await Promise.resolve(); + expect(didResolve).toBe(false); + + acpChatSessionActions.clearPromptCancellation(currentSessionId, 'attempt-1'); + + await waitPromise; + expect(didResolve).toBe(true); + }); + it('removes pending local steer messages when cancellation starts', () => { const currentSessionId = sessionId('session-1'); const localSteerMessage = { @@ -465,6 +519,41 @@ describe('acpChatSessionStore', () => { }); }); + it('resumes streaming only after the final pending user input request resolves', () => { + const currentSessionId = sessionId('session-1'); + + acpChatSessionActions.startPromptAttempt(currentSessionId, 'attempt-1'); + acpChatSessionActions.applyPermissionRequest(permissionRequest(currentSessionId, 'tool-1')); + acpChatSessionActions.applyElicitationRequest(elicitationRequest(currentSessionId)); + + const afterElicitation = acpChatSessionActions.resolveUserInputRequest( + currentSessionId, + acpElicitationUserInputRequestId('acp_elicitation_1') + ); + + expect(afterElicitation?.chatState).toBe(ChatState.WaitingForUserInput); + + const afterPermission = acpChatSessionActions.resolveUserInputRequest( + currentSessionId, + acpPermissionUserInputRequestId('tool-1') + ); + + expect(afterPermission?.chatState).toBe(ChatState.Streaming); + }); + + it('does not resume streaming after user input resolves without an active prompt', () => { + const currentSessionId = sessionId('session-1'); + + acpChatSessionActions.applyPermissionRequest(permissionRequest(currentSessionId, 'tool-1')); + + const snapshot = acpChatSessionActions.resolveUserInputRequest( + currentSessionId, + acpPermissionUserInputRequestId('tool-1') + ); + + expect(snapshot?.chatState).toBe(ChatState.WaitingForUserInput); + }); + it('applies elicitation requests as waiting action-required messages', () => { const currentSessionId = sessionId('session-1'); diff --git a/ui/desktop/src/acp/__tests__/elicitationRequests.test.ts b/ui/desktop/src/acp/__tests__/elicitationRequests.test.ts index c9b691b919f8..c84c3eb0ffa0 100644 --- a/ui/desktop/src/acp/__tests__/elicitationRequests.test.ts +++ b/ui/desktop/src/acp/__tests__/elicitationRequests.test.ts @@ -13,8 +13,10 @@ vi.mock('../../acpChatFeatureFlag', () => ({ })); vi.mock('../chatSessionStore', () => ({ + acpElicitationUserInputRequestId: (elicitationId: string) => `elicitation:${elicitationId}`, acpChatSessionActions: { applyElicitationRequest: vi.fn(), + resolveUserInputRequest: vi.fn(), setElicitationStatus: vi.fn(), }, })); @@ -73,9 +75,8 @@ describe('ACP elicitation requests', () => { await expectStillPending(response); - const appliedRequest = vi.mocked( - acpChatSessionActions.applyElicitationRequest - ).mock.calls[0][0]; + const appliedRequest = vi.mocked(acpChatSessionActions.applyElicitationRequest).mock + .calls[0][0]; expect(appliedRequest.id).toMatch(/^acp_elicitation_/); expect(appliedRequest.sessionId).toBe('session-1'); @@ -91,6 +92,10 @@ describe('ACP elicitation requests', () => { appliedRequest.id, 'submitted' ); + expect(acpChatSessionActions.resolveUserInputRequest).toHaveBeenCalledWith( + 'session-1', + `elicitation:${appliedRequest.id}` + ); await expect(response).resolves.toEqual({ action: 'accept', @@ -142,9 +147,8 @@ describe('ACP elicitation requests', () => { vi.useFakeTimers(); try { const response = requestAcpElicitation(formRequest('session-1')); - const appliedRequest = vi.mocked( - acpChatSessionActions.applyElicitationRequest - ).mock.calls[0][0]; + const appliedRequest = vi.mocked(acpChatSessionActions.applyElicitationRequest).mock + .calls[0][0]; await expectStillPending(response); @@ -155,6 +159,10 @@ describe('ACP elicitation requests', () => { appliedRequest.id, 'cancelled' ); + expect(acpChatSessionActions.resolveUserInputRequest).toHaveBeenCalledWith( + 'session-1', + `elicitation:${appliedRequest.id}` + ); await expect(response).resolves.toEqual({ action: 'cancel' }); expect(resolveAcpElicitationRequest('session-1', appliedRequest.id, {})).toBe(false); } finally { diff --git a/ui/desktop/src/acp/__tests__/permissionRequests.test.ts b/ui/desktop/src/acp/__tests__/permissionRequests.test.ts index d2aeb5d5de0c..9274011fb4b3 100644 --- a/ui/desktop/src/acp/__tests__/permissionRequests.test.ts +++ b/ui/desktop/src/acp/__tests__/permissionRequests.test.ts @@ -5,14 +5,17 @@ import { requestAcpPermission, resolveAcpPermissionRequest, } from '../permissionRequests'; +import { acpChatSessionActions } from '../chatSessionStore'; vi.mock('../../acpChatFeatureFlag', () => ({ USE_ACP_CHAT: true, })); vi.mock('../chatSessionStore', () => ({ + acpPermissionUserInputRequestId: (toolCallId: string) => `permission:${toolCallId}`, acpChatSessionActions: { applyPermissionRequest: vi.fn(), + resolveUserInputRequest: vi.fn(), }, })); @@ -57,6 +60,7 @@ async function expectStillPending(promise: Promise): describe('ACP permission requests', () => { beforeEach(() => { + vi.clearAllMocks(); for (const sessionId of TEST_SESSION_IDS) { cancelAcpPermissionRequestsForSession(sessionId); } @@ -74,6 +78,10 @@ describe('ACP permission requests', () => { await expectStillPending(response); expect(resolveAcpPermissionRequest('session-1', 'tool-1', 'allow_once')).toBe(true); + expect(acpChatSessionActions.resolveUserInputRequest).toHaveBeenCalledWith( + 'session-1', + 'permission:tool-1' + ); await expect(response).resolves.toEqual({ outcome: { outcome: 'selected', diff --git a/ui/desktop/src/acp/chatSessionController.ts b/ui/desktop/src/acp/chatSessionController.ts index cb0bf9db09b6..f7f2363fd7e9 100644 --- a/ui/desktop/src/acp/chatSessionController.ts +++ b/ui/desktop/src/acp/chatSessionController.ts @@ -5,7 +5,7 @@ import { AppEvents } from '../constants/events'; import { ChatState } from '../types/chatState'; import { errorMessage } from '../utils/conversionUtils'; import { showExtensionLoadResults } from '../utils/extensionErrorUtils'; -import { createUserMessage } from '../types/message'; +import { createUserMessage, getPendingToolConfirmationIds } from '../types/message'; import { acpChatSessionActions, acpChatSessionStore, @@ -83,11 +83,21 @@ function assertNoPendingPromptCancellation(sessionId: string): void { } } -function assertNoActivePromptAttempt(sessionId: string): void { - const snapshot = acpChatSessionStore.getSnapshot(sessionId); - if (snapshot?.activePromptAttemptId) { - throw new Error('Cannot update message while prompt is active'); - } +async function forkSessionWithEditedMessage( + sessionId: string, + message: Message, + editedMessage: string +): Promise { + const targetSessionId = await acpForkSession(sessionId, message.created); + + const event = new CustomEvent(AppEvents.SESSION_FORKED, { + detail: { + newSessionId: targetSessionId, + shouldStartAgent: true, + editedMessage, + }, + }); + window.dispatchEvent(event); } async function createSession( @@ -214,37 +224,69 @@ async function updateMessage( options: AcpSubmitMessageOptions ): Promise { assertNoPendingPromptCancellation(sessionId); - assertNoActivePromptAttempt(sessionId); const resolvedEditType = editType ?? 'fork'; const currentSnapshot = options.getCurrentSnapshot(); + const storedSnapshot = acpChatSessionStore.getSnapshot(sessionId); + const activePromptAttemptId = storedSnapshot?.activePromptAttemptId; + const currentMessages = currentSnapshot?.messages ?? []; + const message = currentMessages.find((m) => m.id === messageId); - acpChatSessionActions.setChatState(sessionId, ChatState.Thinking); + if (!message) { + throw new Error(`Message with id ${messageId} not found in current messages`); + } - try { - const currentMessages = currentSnapshot?.messages ?? []; - const message = currentMessages.find((m) => m.id === messageId); + if (resolvedEditType === 'fork') { + await forkSessionWithEditedMessage(sessionId, message, newContent); + return; + } + + const editSnapshot = currentSnapshot ?? storedSnapshot; + const isPendingToolPermission = + editSnapshot?.chatState === ChatState.WaitingForUserInput && + getPendingToolConfirmationIds(editSnapshot?.messages ?? []).size > 0; + const isIdle = editSnapshot?.chatState === ChatState.Idle; + const pendingToolPermissionPromptAttemptId = isPendingToolPermission + ? activePromptAttemptId + : undefined; + const canEditInPlace = isIdle || pendingToolPermissionPromptAttemptId != null; + + if (!canEditInPlace) { + return; + } - if (!message) { - throw new Error(`Message with id ${messageId} not found in current messages`); + if (pendingToolPermissionPromptAttemptId != null) { + const cancellation = acpChatSessionActions.startPromptCancellation( + sessionId, + pendingToolPermissionPromptAttemptId + ); + if (!cancellation) { + throw new Error('Cannot update message while prompt is active'); } - if (resolvedEditType === 'fork') { - const targetSessionId = await acpForkSession(sessionId, message.created); - - acpChatSessionActions.setChatState(sessionId, ChatState.Idle); - const event = new CustomEvent(AppEvents.SESSION_FORKED, { - detail: { - newSessionId: targetSessionId, - shouldStartAgent: true, - editedMessage: newContent, - }, - }); - window.dispatchEvent(event); - window.electron.logInfo(`Dispatched session-forked event for session ${targetSessionId}`); - return; + const promptCancellationSettled = acpChatSessionActions.waitForPromptCancellation( + sessionId, + pendingToolPermissionPromptAttemptId + ); + + try { + await acpCancelPrompt(sessionId); + } catch { + acpChatSessionActions.restorePromptCancellation( + sessionId, + pendingToolPermissionPromptAttemptId + ); + throw new Error('Cannot update message because the active prompt could not be cancelled'); } + cancelAcpPermissionRequestsForSession(sessionId); + cancelAcpElicitationRequestsForSession(sessionId); + await promptCancellationSettled; + } + + acpChatSessionActions.setChatState(sessionId, ChatState.Thinking); + + try { await acpTruncateSessionConversation(sessionId, message.created); const truncatedMessages = currentMessages.filter((m) => m.created < message.created); diff --git a/ui/desktop/src/acp/chatSessionStore.ts b/ui/desktop/src/acp/chatSessionStore.ts index 08047dc45bf7..c7901543a4ac 100644 --- a/ui/desktop/src/acp/chatSessionStore.ts +++ b/ui/desktop/src/acp/chatSessionStore.ts @@ -29,6 +29,12 @@ type SnapshotListener = (snapshot: AcpChatSessionSnapshot) => void; interface StoreEntry extends AcpChatSessionSnapshot { adapter: AcpSessionNotificationAdapter; + promptCancellationRestoreState: { + activeRunId: string | null; + chatState: ChatState; + pendingUserInputRequestIds: Set; + } | null; + pendingUserInputRequestIds: Set; pendingLocalSteerMessageIds: Set; } @@ -72,6 +78,10 @@ export interface AcpChatSessionActions { setMessages(sessionId: string, messages: Message[]): AcpChatSessionSnapshot; addPendingLocalSteerMessage(sessionId: string, message: Message): AcpChatSessionSnapshot; setChatState(sessionId: string, chatState: ChatState): AcpChatSessionSnapshot; + resolveUserInputRequest( + sessionId: string, + userInputRequestId: string + ): AcpChatSessionSnapshot | undefined; startPromptAttempt(sessionId: string, promptAttemptId: string): AcpChatSessionSnapshot; startPromptCancellation( @@ -82,6 +92,11 @@ export interface AcpChatSessionActions { sessionId: string, promptAttemptId: string ): AcpChatSessionSnapshot | undefined; + restorePromptCancellation( + sessionId: string, + promptAttemptId: string + ): AcpChatSessionSnapshot | undefined; + waitForPromptCancellation(sessionId: string, promptAttemptId: string): Promise; finishPromptAttemptIfCurrent(sessionId: string, promptAttemptId: string, error?: string): boolean; clearActivePromptAttempt(sessionId: string): AcpChatSessionSnapshot | undefined; isCurrentPromptAttempt(sessionId: string, promptAttemptId: string): boolean; @@ -144,6 +159,8 @@ function createAcpChatSessionStoreInternal(): AcpChatSessionStoreInternal { activePromptAttemptId: null, activeRunId: null, pendingCancelPromptAttemptId: null, + promptCancellationRestoreState: null, + pendingUserInputRequestIds: new Set(), pendingLocalSteerMessageIds: new Set(), adapter: createAcpSessionNotificationAdapter(), }; @@ -223,6 +240,29 @@ function createAcpChatSessionStoreInternal(): AcpChatSessionStoreInternal { return notify(sessionId, entry); }; + const resolveUserInputRequest: AcpChatSessionActions['resolveUserInputRequest'] = ( + sessionId, + userInputRequestId + ) => { + const entry = sessionsById.get(sessionId); + if (!entry) { + return undefined; + } + + entry.pendingUserInputRequestIds.delete(userInputRequestId); + + if ( + entry.activePromptAttemptId && + entry.chatState === ChatState.WaitingForUserInput && + entry.pendingUserInputRequestIds.size === 0 + ) { + entry.chatState = ChatState.Streaming; + return notify(sessionId, entry); + } + + return snapshotFromEntry(entry); + }; + const setSessionLoadError: AcpChatSessionActions['setSessionLoadError'] = ( sessionId, sessionLoadError @@ -241,6 +281,8 @@ function createAcpChatSessionStoreInternal(): AcpChatSessionStoreInternal { entry.activePromptAttemptId = promptAttemptId; entry.activeRunId = null; entry.pendingCancelPromptAttemptId = null; + entry.promptCancellationRestoreState = null; + entry.pendingUserInputRequestIds.clear(); entry.chatState = ChatState.Streaming; entry.sessionLoadError = undefined; entry.notifications = []; @@ -256,9 +298,15 @@ function createAcpChatSessionStoreInternal(): AcpChatSessionStoreInternal { return undefined; } + entry.promptCancellationRestoreState = { + activeRunId: entry.activeRunId, + chatState: entry.chatState, + pendingUserInputRequestIds: new Set(entry.pendingUserInputRequestIds), + }; entry.activePromptAttemptId = null; entry.activeRunId = null; entry.pendingCancelPromptAttemptId = promptAttemptId; + entry.pendingUserInputRequestIds.clear(); discardPendingLocalSteerMessages(entry); entry.chatState = ChatState.Idle; return notify(sessionId, entry); @@ -274,9 +322,52 @@ function createAcpChatSessionStoreInternal(): AcpChatSessionStoreInternal { } entry.pendingCancelPromptAttemptId = null; + entry.promptCancellationRestoreState = null; + return notify(sessionId, entry); + }; + + const restorePromptCancellation: AcpChatSessionActions['restorePromptCancellation'] = ( + sessionId, + promptAttemptId + ) => { + const entry = sessionsById.get(sessionId); + if ( + !entry || + entry.pendingCancelPromptAttemptId !== promptAttemptId || + !entry.promptCancellationRestoreState + ) { + return undefined; + } + + const restoreState = entry.promptCancellationRestoreState; + entry.activePromptAttemptId = promptAttemptId; + entry.activeRunId = restoreState.activeRunId; + entry.pendingCancelPromptAttemptId = null; + entry.promptCancellationRestoreState = null; + entry.pendingUserInputRequestIds = new Set(restoreState.pendingUserInputRequestIds); + entry.chatState = restoreState.chatState; return notify(sessionId, entry); }; + const waitForPromptCancellation: AcpChatSessionActions['waitForPromptCancellation'] = ( + sessionId, + promptAttemptId + ) => { + const entry = sessionsById.get(sessionId); + if (!entry || entry.pendingCancelPromptAttemptId !== promptAttemptId) { + return Promise.resolve(); + } + + return new Promise((resolve) => { + const unsubscribe = subscribe(sessionId, (snapshot) => { + if (snapshot.pendingCancelPromptAttemptId !== promptAttemptId) { + unsubscribe(); + resolve(); + } + }); + }); + }; + const finishPromptAttemptIfCurrent: AcpChatSessionActions['finishPromptAttemptIfCurrent'] = ( sessionId, promptAttemptId, @@ -290,6 +381,8 @@ function createAcpChatSessionStoreInternal(): AcpChatSessionStoreInternal { entry.activePromptAttemptId = null; entry.activeRunId = null; entry.pendingCancelPromptAttemptId = null; + entry.promptCancellationRestoreState = null; + entry.pendingUserInputRequestIds.clear(); discardPendingLocalSteerMessages(entry); entry.chatState = ChatState.Idle; entry.sessionLoadError = error; @@ -307,6 +400,7 @@ function createAcpChatSessionStoreInternal(): AcpChatSessionStoreInternal { entry.activePromptAttemptId = null; entry.activeRunId = null; + entry.pendingUserInputRequestIds.clear(); discardPendingLocalSteerMessages(entry); entry.chatState = ChatState.Idle; return notify(sessionId, entry); @@ -338,6 +432,9 @@ function createAcpChatSessionStoreInternal(): AcpChatSessionStoreInternal { const entry = getOrCreateEntry(request.sessionId); const changes = entry.adapter.applyPermissionRequest(request); applyChatStateChanges(entry, changes); + entry.pendingUserInputRequestIds.add( + acpPermissionUserInputRequestId(request.toolCall.toolCallId) + ); entry.chatState = ChatState.WaitingForUserInput; return notify(request.sessionId, entry); }; @@ -346,6 +443,7 @@ function createAcpChatSessionStoreInternal(): AcpChatSessionStoreInternal { const entry = getOrCreateEntry(request.sessionId); const changes = entry.adapter.applyElicitationRequest(request); applyChatStateChanges(entry, changes); + entry.pendingUserInputRequestIds.add(acpElicitationUserInputRequestId(request.id)); entry.chatState = ChatState.WaitingForUserInput; return notify(request.sessionId, entry); }; @@ -381,9 +479,12 @@ function createAcpChatSessionStoreInternal(): AcpChatSessionStoreInternal { setMessages, addPendingLocalSteerMessage, setChatState, + resolveUserInputRequest, startPromptAttempt, startPromptCancellation, clearPromptCancellation, + restorePromptCancellation, + waitForPromptCancellation, finishPromptAttemptIfCurrent, clearActivePromptAttempt, isCurrentPromptAttempt, @@ -456,9 +557,12 @@ function actionsFromStore(store: AcpChatSessionStoreInternal): AcpChatSessionAct setMessages: store.setMessages, addPendingLocalSteerMessage: store.addPendingLocalSteerMessage, setChatState: store.setChatState, + resolveUserInputRequest: store.resolveUserInputRequest, startPromptAttempt: store.startPromptAttempt, startPromptCancellation: store.startPromptCancellation, clearPromptCancellation: store.clearPromptCancellation, + restorePromptCancellation: store.restorePromptCancellation, + waitForPromptCancellation: store.waitForPromptCancellation, finishPromptAttemptIfCurrent: store.finishPromptAttemptIfCurrent, clearActivePromptAttempt: store.clearActivePromptAttempt, isCurrentPromptAttempt: store.isCurrentPromptAttempt, @@ -499,10 +603,20 @@ function resetReplayState(entry: StoreEntry): void { entry.notifications = []; entry.activeRunId = null; entry.pendingCancelPromptAttemptId = null; + entry.promptCancellationRestoreState = null; + entry.pendingUserInputRequestIds.clear(); entry.pendingLocalSteerMessageIds.clear(); entry.adapter = createAcpSessionNotificationAdapter(); } +export function acpPermissionUserInputRequestId(toolCallId: string): string { + return `permission:${toolCallId}`; +} + +export function acpElicitationUserInputRequestId(elicitationId: string): string { + return `elicitation:${elicitationId}`; +} + function retainPendingLocalSteerMessageIds(entry: StoreEntry): void { if (entry.pendingLocalSteerMessageIds.size === 0) { return; diff --git a/ui/desktop/src/acp/config.ts b/ui/desktop/src/acp/config.ts new file mode 100644 index 000000000000..7ca81030bba3 --- /dev/null +++ b/ui/desktop/src/acp/config.ts @@ -0,0 +1,38 @@ +import { getAcpClient } from './acpConnection'; + +export type ConfigReadValue = unknown; + +export async function acpReadConfig( + key: string, + isSecret: boolean = false +): Promise { + const client = await getAcpClient(); + const { value } = await client.goose.configRead_unstable({ key, isSecret }); + if (value == null) { + return null; + } + if (isSecret) { + return { maskedValue: value as string }; + } + return value; +} + +export async function acpUpsertConfig( + key: string, + value: unknown, + isSecret: boolean = false +): Promise { + const client = await getAcpClient(); + await client.goose.configUpsert_unstable({ key, value, isSecret }); +} + +export async function acpRemoveConfig(key: string, isSecret: boolean): Promise { + const client = await getAcpClient(); + await client.goose.configRemove_unstable({ key, isSecret }); +} + +export async function acpReadAllConfig(): Promise> { + const client = await getAcpClient(); + const { config } = await client.goose.configReadAll_unstable({}); + return config; +} diff --git a/ui/desktop/src/acp/elicitationRequests.ts b/ui/desktop/src/acp/elicitationRequests.ts index 19caf7f63c61..9ec139d1a31a 100644 --- a/ui/desktop/src/acp/elicitationRequests.ts +++ b/ui/desktop/src/acp/elicitationRequests.ts @@ -6,7 +6,7 @@ import type { } from '@agentclientprotocol/sdk'; import { v7 as uuidv7 } from 'uuid'; import { USE_ACP_CHAT } from '../acpChatFeatureFlag'; -import { acpChatSessionActions } from './chatSessionStore'; +import { acpChatSessionActions, acpElicitationUserInputRequestId } from './chatSessionStore'; type SessionScopedFormElicitationRequest = CreateElicitationRequest & { mode: 'form'; @@ -56,6 +56,10 @@ export async function requestAcpElicitation( elicitationRequest.id, 'cancelled' ); + acpChatSessionActions.resolveUserInputRequest( + elicitationRequest.sessionId, + acpElicitationUserInputRequestId(elicitationRequest.id) + ); pending.resolve(cancelledElicitationResponse()); }, ACP_ELICITATION_TIMEOUT_SECONDS * 1000); @@ -78,6 +82,10 @@ export function resolveAcpElicitationRequest( pendingRequests.delete(key); clearTimeout(pending.timeoutId); acpChatSessionActions.setElicitationStatus(sessionId, elicitationId, 'submitted'); + acpChatSessionActions.resolveUserInputRequest( + sessionId, + acpElicitationUserInputRequestId(elicitationId) + ); pending.resolve(acceptedElicitationResponse(userData)); return true; } diff --git a/ui/desktop/src/acp/permissionRequests.ts b/ui/desktop/src/acp/permissionRequests.ts index 847ad141feb3..c6f2167eb41e 100644 --- a/ui/desktop/src/acp/permissionRequests.ts +++ b/ui/desktop/src/acp/permissionRequests.ts @@ -1,7 +1,7 @@ import type { RequestPermissionRequest, RequestPermissionResponse } from '@agentclientprotocol/sdk'; import type { Permission } from '../api'; import { USE_ACP_CHAT } from '../acpChatFeatureFlag'; -import { acpChatSessionActions } from './chatSessionStore'; +import { acpChatSessionActions, acpPermissionUserInputRequestId } from './chatSessionStore'; interface PendingPermissionRequest { request: RequestPermissionRequest; @@ -41,6 +41,10 @@ export function resolveAcpPermissionRequest( } pendingRequests.delete(key); + acpChatSessionActions.resolveUserInputRequest( + sessionId, + acpPermissionUserInputRequestId(toolCallId) + ); pending.resolve(permissionResponseForAction(pending.request, action)); return true; } diff --git a/ui/desktop/src/acp/permissions.ts b/ui/desktop/src/acp/permissions.ts new file mode 100644 index 000000000000..c88dc9119d01 --- /dev/null +++ b/ui/desktop/src/acp/permissions.ts @@ -0,0 +1,18 @@ +import type { ToolListItem, ToolPermissionEntry, ToolPermissionLevel } from '@aaif/goose-sdk'; +import { getAcpClient } from './acpConnection'; + +export type { ToolListItem, ToolPermissionEntry, ToolPermissionLevel }; + +export async function listTools(sessionId: string, extensionName?: string): Promise { + const client = await getAcpClient(); + const response = await client.goose.toolsList_unstable({ + sessionId, + extensionName: extensionName ?? null, + }); + return response.tools ?? []; +} + +export async function setToolPermissions(toolPermissions: ToolPermissionEntry[]): Promise { + const client = await getAcpClient(); + await client.goose.toolsPermissionsSet_unstable({ toolPermissions }); +} diff --git a/ui/desktop/src/acp/prompts.ts b/ui/desktop/src/acp/prompts.ts new file mode 100644 index 000000000000..8a3a7329f0d5 --- /dev/null +++ b/ui/desktop/src/acp/prompts.ts @@ -0,0 +1,29 @@ +import type { + GetPromptResponse_unstable, + PromptTemplateEntry, +} from '@aaif/goose-sdk'; +import { getAcpClient } from './acpConnection'; + +export type PromptTemplate = PromptTemplateEntry; +export type PromptContent = GetPromptResponse_unstable; + +export async function acpListPrompts(): Promise { + const client = await getAcpClient(); + const response = await client.goose.configPromptsList_unstable({}); + return response.prompts; +} + +export async function acpGetPrompt(name: string): Promise { + const client = await getAcpClient(); + return client.goose.configPromptsGet_unstable({ name }); +} + +export async function acpSavePrompt(name: string, content: string): Promise { + const client = await getAcpClient(); + await client.goose.configPromptsSave_unstable({ name, content }); +} + +export async function acpResetPrompt(name: string): Promise { + const client = await getAcpClient(); + await client.goose.configPromptsReset_unstable({ name }); +} diff --git a/ui/desktop/src/acp/providers.ts b/ui/desktop/src/acp/providers.ts new file mode 100644 index 000000000000..933559a17ad2 --- /dev/null +++ b/ui/desktop/src/acp/providers.ts @@ -0,0 +1,217 @@ +import type { + CanonicalModelInfoDto, + CustomProviderCreateRequest_unstable, + CustomProviderReadResponse_unstable, + ProviderSecretDto, + ProviderTemplateCatalogEntryDto, + ProviderTemplateDto, +} from '@aaif/goose-sdk'; +import type { ProviderDetails, ThinkingEffort, UpdateCustomProviderRequest } from '../api'; +import { getAcpClient } from './acpConnection'; + +export type { CanonicalModelInfoDto, ProviderSecretDto }; + +function updateRequestToCreate( + request: UpdateCustomProviderRequest +): CustomProviderCreateRequest_unstable { + return { + engine: request.engine, + displayName: request.display_name, + apiUrl: request.api_url, + apiKey: request.api_key || null, + models: request.models, + supportsStreaming: request.supports_streaming ?? null, + headers: request.headers ?? undefined, + requiresAuth: request.requires_auth ?? true, + catalogProviderId: request.catalog_provider_id ?? null, + basePath: request.base_path ?? null, + preservesThinking: request.preserves_thinking ?? null, + }; +} + +export async function acpListProviderDetails(): Promise { + const client = await getAcpClient(); + const { entries } = await client.goose.providersList_unstable({}); + return entries.map((entry) => ({ + name: entry.providerId, + is_configured: entry.configured, + provider_type: entry.providerType as ProviderDetails['provider_type'], + metadata: { + name: entry.providerId, + display_name: entry.providerName, + description: entry.description, + default_model: entry.defaultModel, + model_doc_link: '', + model_selection_hint: entry.modelSelectionHint ?? null, + config_keys: entry.configKeys.map((key) => ({ + name: key.name, + required: key.required, + secret: key.secret, + default: key.default ?? null, + oauth_flow: key.oauthFlow ?? false, + device_code_flow: key.deviceCodeFlow ?? false, + primary: key.primary ?? false, + })), + known_models: entry.models.map((model) => ({ + name: model.id, + context_limit: model.contextLimit ?? 0, + reasoning: model.reasoning ?? undefined, + })), + setup_steps: entry.setupSteps, + }, + })); +} + +export async function acpListProviderModels(providerId: string) { + const client = await getAcpClient(); + const { entries } = await client.goose.providersList_unstable({ providerIds: [providerId] }); + return entries.find((e) => e.providerId === providerId)?.models ?? []; +} + +export async function acpListProviderCatalogEntries( + format?: string +): Promise { + const client = await getAcpClient(); + const { providers } = await client.goose.providersCatalogList_unstable(format ? { format } : {}); + return providers; +} + +export async function acpGetProviderTemplate(providerId: string): Promise { + const client = await getAcpClient(); + const { template } = await client.goose.providersCatalogTemplate_unstable({ providerId }); + return template; +} + +export async function acpGetCustomProvider( + providerId: string +): Promise { + const client = await getAcpClient(); + return client.goose.providersCustomRead_unstable({ providerId }); +} + +export async function acpCreateCustomProviderFromRequest( + request: UpdateCustomProviderRequest +): Promise<{ provider_name: string }> { + const client = await getAcpClient(); + const response = await client.goose.providersCustomCreate_unstable( + updateRequestToCreate(request) + ); + return { provider_name: response.providerId }; +} + +export async function acpUpdateCustomProviderFromRequest( + providerId: string, + request: UpdateCustomProviderRequest +): Promise { + const client = await getAcpClient(); + await client.goose.providersCustomUpdate_unstable({ + providerId, + ...updateRequestToCreate(request), + }); +} + +export async function acpDeleteCustomProvider(providerId: string): Promise { + const client = await getAcpClient(); + await client.goose.providersCustomDelete_unstable({ providerId }); +} + +export async function acpReadProviderConfig(providerId: string) { + const client = await getAcpClient(); + const { fields } = await client.goose.providersConfigRead_unstable({ providerId }); + return fields; +} + +export async function acpDeleteProviderConfig(providerId: string): Promise { + const client = await getAcpClient(); + await client.goose.providersConfigDelete_unstable({ providerId }); +} + +export async function acpSaveProviderConfig( + providerId: string, + fields: { key: string; value: string }[] +): Promise { + const client = await getAcpClient(); + await client.goose.providersConfigSave_unstable({ providerId, fields }); +} + +export async function acpAuthenticateProvider(providerId: string): Promise { + const client = await getAcpClient(); + await client.goose.providersConfigAuthenticate_unstable({ providerId }); +} + +export async function acpListProviderSecrets(): Promise { + const client = await getAcpClient(); + const { secrets } = await client.goose.providersSecretsList_unstable({}); + return secrets; +} + +export async function acpDeleteProviderSecret(id: string): Promise { + const client = await getAcpClient(); + await client.goose.providersSecretsDelete_unstable({ id }); +} + +export async function acpGetCanonicalModelInfo( + provider: string, + model: string +): Promise { + const client = await getAcpClient(); + const { modelInfo } = await client.goose.providersCanonicalModelInfo_unstable({ + provider, + model, + }); + return modelInfo ?? null; +} + +export async function acpReadDefaults(): Promise<{ + providerId: string | null; + modelId: string | null; +}> { + const client = await getAcpClient(); + const response = await client.goose.defaultsRead_unstable({}); + return { + providerId: response.providerId ?? null, + modelId: response.modelId ?? null, + }; +} + +export async function acpSaveDefaults(providerId: string, modelId?: string | null): Promise { + const client = await getAcpClient(); + await client.goose.defaultsSave_unstable({ providerId, modelId: modelId ?? null }); +} + +export async function acpClearDefaults(): Promise { + const client = await getAcpClient(); + await client.goose.defaultsClear_unstable({}); +} + +export async function acpReadThinkingEffort(): Promise { + const client = await getAcpClient(); + const response = await client.goose.preferencesRead_unstable({ keys: ['gooseThinkingEffort'] }); + const value = response.values.find((v) => v.key === 'gooseThinkingEffort')?.value; + return typeof value === 'string' ? (value as ThinkingEffort) : null; +} + +export async function acpSaveThinkingEffort(effort: ThinkingEffort): Promise { + const client = await getAcpClient(); + await client.goose.preferencesSave_unstable({ + values: [{ key: 'gooseThinkingEffort', value: effort }], + }); +} + +/** + * Switch the provider (and model) for an active session via ACP config options. + * + * Changing the provider on the server resets the session's model, so the model + * is applied as a follow-up step when supplied. + */ +export async function acpSetSessionProviderModel( + sessionId: string, + providerId: string, + modelId?: string | null +): Promise { + const client = await getAcpClient(); + await client.setSessionConfigOption({ sessionId, configId: 'provider', value: providerId }); + if (modelId) { + await client.setSessionConfigOption({ sessionId, configId: 'model', value: modelId }); + } +} diff --git a/ui/desktop/src/api/index.ts b/ui/desktop/src/api/index.ts index 783ac1482577..412a7ad918b9 100644 --- a/ui/desktop/src/api/index.ts +++ b/ui/desktop/src/api/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts -export { addExtension, agentAddExtension, agentRemoveExtension, cancelDownload, cancelLocalModelDownload, checkProvider, cleanupProviderCache, configureProviderOauth, confirmToolAction, createCustomProvider, createSchedule, decodeRecipe, deleteLocalModel, deleteModel, deleteProviderSecret, deleteRecipe, deleteSchedule, diagnostics, downloadHfModel, downloadModel, encodeRecipe, forkSession, getCanonicalModelInfo, getCustomProvider, getDictationConfig, getDownloadProgress, getExtensions, getLocalModelDownloadProgress, getModelSettings, getPrompt, getPrompts, getProviderCatalog, getProviderCatalogTemplate, getProviderModelInfo, getProviderModels, getRepoFiles, getSession, getSessionExtensions, getSlashCommands, getTools, getTunnelStatus, inspectRunningJob, killRunningJob, listBuiltinChatTemplates, listLocalModels, listModels, listProviderSecrets, listRecipes, listSchedules, mcpUiProxy, type Options, parseRecipe, pauseSchedule, providers, readAllConfig, readConfig, recipeToYaml, removeConfig, removeCustomProvider, removeExtension, reply, resetPrompt, restartAgent, resumeAgent, runNowHandler, savePrompt, saveRecipe, scanRecipe, scheduleRecipe, searchHfModels, sendTelemetryEvent, sessionCancel, sessionEvents, sessionReply, sessionsHandler, setConfigProvider, setRecipeSlashCommand, startAgent, startNanogptSetup, startOpenrouterSetup, startTetrateSetup, status, stopAgent, syncFeaturedModels, systemInfo, transcribeDictation, unpauseSchedule, updateAgentProvider, updateCustomProvider, updateFromSession, updateModelSettings, updateSchedule, updateSession, updateSessionName, updateSessionUserRecipeValues, updateWorkingDir, upsertConfig, upsertPermissions, validateConfig } from './sdk.gen'; -export type { ActionRequired, ActionRequiredData, AddExtensionData, AddExtensionErrors, AddExtensionRequest, AddExtensionResponse, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponse, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponse, AgentRemoveExtensionResponses, Annotations, Author, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CancelRequest, ChatRequest, ChatTemplate, CheckProviderData, CheckProviderRequest, CleanupProviderCacheData, CleanupProviderCacheErrors, CleanupProviderCacheResponse, CleanupProviderCacheResponses, ClientOptions, CommandType, ConfigKey, ConfigKeyQuery, ConfigResponse, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionRequest, ConfirmToolActionResponses, Content, ContentBlock, Conversation, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponse, CreateCustomProviderResponse2, CreateCustomProviderResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleRequest, CreateScheduleResponse, CreateScheduleResponses, CspMetadata, DeclarativeProviderConfig, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeRequest, DecodeRecipeResponse, DecodeRecipeResponse2, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteProviderSecretData, DeleteProviderSecretErrors, DeleteProviderSecretResponse, DeleteProviderSecretResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeRequest, DeleteRecipeResponse, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponse, DeleteScheduleResponses, DiagnosticsConfig, DiagnosticsData, DiagnosticsError, DiagnosticsErrors, DiagnosticsExtensions, DiagnosticsLevel, DiagnosticsLogs, DiagnosticsPrompt, DiagnosticsReport, DiagnosticsResponse, DiagnosticsResponses, DiagnosticsScheduledRecipe, DiagnosticsTextFile, DictationProvider, DictationProviderStatus, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponse, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelRequest, DownloadModelResponses, DownloadProgress, DownloadStatus, EmbeddedResource, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeRequest, EncodeRecipeResponse, EncodeRecipeResponse2, EncodeRecipeResponses, Envs, EnvVarConfig, ErrorResponse, ExtensionConfig, ExtensionData, ExtensionEntry, ExtensionLoadResult, ExtensionQuery, ExtensionResponse, ForkRequest, ForkResponse, ForkSessionData, ForkSessionErrors, ForkSessionResponse, ForkSessionResponses, FrontendToolRequest, GetCanonicalModelInfoData, GetCanonicalModelInfoResponse, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponse, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponse, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponse, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponse, GetExtensionsResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponse, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponse, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponse, GetPromptResponses, GetPromptsData, GetPromptsResponse, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponse, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponse, GetProviderCatalogTemplateResponses, GetProviderModelInfoData, GetProviderModelInfoErrors, GetProviderModelInfoResponse, GetProviderModelInfoResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponse, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponse, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponse, GetSessionExtensionsResponses, GetSessionResponse, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponse, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsQuery, GetToolsResponse, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponse, GetTunnelStatusResponses, GooseApp, GooseMode, HfGgufFile, HfModelInfo, HfModelVariant, HfQuantVariant, Icon, IconTheme, ImageContent, InferenceMetadata, InspectJobResponse, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponse, InspectRunningJobResponses, JsonObject, KillJobResponse, KillRunningJobData, KillRunningJobResponses, ListBuiltinChatTemplatesData, ListBuiltinChatTemplatesResponse, ListBuiltinChatTemplatesResponses, ListLocalModelsData, ListLocalModelsResponse, ListLocalModelsResponses, ListModelsData, ListModelsResponse, ListModelsResponses, ListProviderSecretsData, ListProviderSecretsErrors, ListProviderSecretsResponse, ListProviderSecretsResponses, ListRecipeResponse, ListRecipesData, ListRecipesErrors, ListRecipesResponse, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponse, ListSchedulesResponse2, ListSchedulesResponses, LoadedProvider, LocalModelResponse, McpAppResource, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, Message, MessageContent, MessageEvent, MessageMetadata, ModelCapabilities, ModelConfig, ModelDownloadStatus, ModelInfo, ModelInfoData, ModelInfoQuery, ModelInfoResponse, ModelSettings, ModelTemplate, ParseRecipeData, ParseRecipeError, ParseRecipeErrors, ParseRecipeRequest, ParseRecipeResponse, ParseRecipeResponse2, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponse, PauseScheduleResponses, Permission, PermissionLevel, PermissionsMetadata, PrincipalType, PromptContentResponse, PromptsListResponse, ProviderCatalogEntry, ProviderDetails, ProviderEngine, ProviderMetadata, ProviderModelInfoQuery, ProvidersData, ProviderSecret, ProviderSecretsResponse, ProviderSecretStatus, ProviderSecretStorage, ProvidersResponse, ProvidersResponse2, ProvidersResponses, ProviderTemplate, ProviderType, RawAudioContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ReadAllConfigData, ReadAllConfigResponse, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, Recipe, RecipeManifest, RecipeParameter, RecipeParameterInputType, RecipeParameterRequirement, RecipeToYamlData, RecipeToYamlError, RecipeToYamlErrors, RecipeToYamlRequest, RecipeToYamlResponse, RecipeToYamlResponse2, RecipeToYamlResponses, RedactedThinkingContent, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponse, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponse, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionRequest, RemoveExtensionResponse, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponse, ReplyResponses, RepoVariantsResponse, ResetPromptData, ResetPromptErrors, ResetPromptResponse, ResetPromptResponses, ResourceContents, ResourceMetadata, Response, RestartAgentData, RestartAgentErrors, RestartAgentRequest, RestartAgentResponse, RestartAgentResponse2, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentRequest, ResumeAgentResponse, ResumeAgentResponse2, ResumeAgentResponses, RetryConfig, Role, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponse, RunNowHandlerResponses, RunNowResponse, SamplingConfig, SavePromptData, SavePromptErrors, SavePromptRequest, SavePromptResponse, SavePromptResponses, SaveRecipeData, SaveRecipeError, SaveRecipeErrors, SaveRecipeRequest, SaveRecipeResponse, SaveRecipeResponse2, SaveRecipeResponses, ScanRecipeData, ScanRecipeRequest, ScanRecipeResponse, ScanRecipeResponse2, ScanRecipeResponses, ScheduledJob, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeRequest, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponse, SearchHfModelsResponses, SendTelemetryEventData, SendTelemetryEventResponses, Session, SessionCancelData, SessionCancelResponses, SessionDisplayInfo, SessionEventsData, SessionEventsErrors, SessionEventsResponse, SessionEventsResponses, SessionExtensionsResponse, SessionReplyData, SessionReplyErrors, SessionReplyRequest, SessionReplyResponse, SessionReplyResponse2, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponse, SessionsHandlerResponses, SessionsQuery, SessionType, SetConfigProviderData, SetProviderRequest, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, SetSlashCommandRequest, Settings, SetupResponse, SlashCommand, SlashCommandsResponse, StartAgentData, StartAgentError, StartAgentErrors, StartAgentRequest, StartAgentResponse, StartAgentResponses, StartNanogptSetupData, StartNanogptSetupResponse, StartNanogptSetupResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponse, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponse, StartTetrateSetupResponses, StatusData, StatusResponse, StatusResponses, StopAgentData, StopAgentErrors, StopAgentRequest, StopAgentResponse, StopAgentResponses, SubRecipe, SuccessCheck, SyncFeaturedModelsData, SyncFeaturedModelsResponses, SystemInfo, SystemInfoData, SystemInfoResponse, SystemInfoResponses, SystemNotificationContent, SystemNotificationType, TaskSupport, TelemetryEventRequest, Template, TextContent, ThinkingContent, ThinkingEffort, TokenState, Tool, ToolAnnotations, ToolCallingMode, ToolConfirmationRequest, ToolExecution, ToolInfo, ToolPermission, ToolRequest, ToolResponse, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponse, TranscribeDictationResponses, TranscribeRequest, TranscribeResponse, TunnelInfo, TunnelState, UiMetadata, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponse, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderRequest, UpdateCustomProviderResponse, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionRequest, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponse, UpdateModelSettingsResponses, UpdateProviderRequest, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleRequest, UpdateScheduleResponse, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameRequest, UpdateSessionNameResponses, UpdateSessionRequest, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesError, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesRequest, UpdateSessionUserRecipeValuesResponse, UpdateSessionUserRecipeValuesResponse2, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirRequest, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigQuery, UpsertConfigResponse, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsQuery, UpsertPermissionsResponse, UpsertPermissionsResponses, Usage, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponse, ValidateConfigResponses, WhisperModelResponse, WindowProps } from './types.gen'; +export { addExtension, agentAddExtension, agentRemoveExtension, cancelDownload, cancelLocalModelDownload, checkProvider, cleanupProviderCache, confirmToolAction, createCustomProvider, createSchedule, decodeRecipe, deleteLocalModel, deleteModel, deleteProviderSecret, deleteRecipe, deleteSchedule, diagnostics, downloadHfModel, downloadModel, encodeRecipe, forkSession, getCanonicalModelInfo, getCustomProvider, getDictationConfig, getDownloadProgress, getExtensions, getLocalModelDownloadProgress, getModelSettings, getPrompt, getPrompts, getProviderCatalog, getProviderCatalogTemplate, getProviderModelInfo, getProviderModels, getRepoFiles, getSession, getSessionExtensions, getSlashCommands, getTools, inspectRunningJob, killRunningJob, listBuiltinChatTemplates, listLocalModels, listModels, listProviderSecrets, listRecipes, listSchedules, mcpUiProxy, type Options, parseRecipe, pauseSchedule, providers, readAllConfig, readConfig, recipeToYaml, removeConfig, removeCustomProvider, removeExtension, reply, resetPrompt, restartAgent, resumeAgent, runNowHandler, savePrompt, saveRecipe, scanRecipe, scheduleRecipe, searchHfModels, sendTelemetryEvent, sessionCancel, sessionEvents, sessionReply, sessionsHandler, setConfigProvider, setRecipeSlashCommand, startAgent, startNanogptSetup, startOpenrouterSetup, startTetrateSetup, status, stopAgent, syncFeaturedModels, systemInfo, transcribeDictation, unpauseSchedule, updateAgentProvider, updateCustomProvider, updateFromSession, updateModelSettings, updateSchedule, updateSession, updateSessionName, updateSessionUserRecipeValues, updateWorkingDir, upsertConfig, validateConfig } from './sdk.gen'; +export type { ActionRequired, ActionRequiredData, AddExtensionData, AddExtensionErrors, AddExtensionRequest, AddExtensionResponse, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponse, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponse, AgentRemoveExtensionResponses, Annotations, Author, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CancelRequest, ChatRequest, ChatTemplate, CheckProviderData, CheckProviderRequest, CleanupProviderCacheData, CleanupProviderCacheErrors, CleanupProviderCacheResponse, CleanupProviderCacheResponses, ClientOptions, CommandType, ConfigKey, ConfigKeyQuery, ConfigResponse, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionRequest, ConfirmToolActionResponses, Content, ContentBlock, Conversation, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponse, CreateCustomProviderResponse2, CreateCustomProviderResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleRequest, CreateScheduleResponse, CreateScheduleResponses, CspMetadata, DeclarativeProviderConfig, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeRequest, DecodeRecipeResponse, DecodeRecipeResponse2, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteProviderSecretData, DeleteProviderSecretErrors, DeleteProviderSecretResponse, DeleteProviderSecretResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeRequest, DeleteRecipeResponse, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponse, DeleteScheduleResponses, DiagnosticsConfig, DiagnosticsData, DiagnosticsError, DiagnosticsErrors, DiagnosticsExtensions, DiagnosticsLevel, DiagnosticsLogs, DiagnosticsPrompt, DiagnosticsReport, DiagnosticsResponse, DiagnosticsResponses, DiagnosticsScheduledRecipe, DiagnosticsTextFile, DictationProvider, DictationProviderStatus, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponse, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelRequest, DownloadModelResponses, DownloadProgress, DownloadStatus, EmbeddedResource, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeRequest, EncodeRecipeResponse, EncodeRecipeResponse2, EncodeRecipeResponses, Envs, EnvVarConfig, ErrorResponse, ExtensionConfig, ExtensionData, ExtensionEntry, ExtensionLoadResult, ExtensionQuery, ExtensionResponse, ForkRequest, ForkResponse, ForkSessionData, ForkSessionErrors, ForkSessionResponse, ForkSessionResponses, FrontendToolRequest, GetCanonicalModelInfoData, GetCanonicalModelInfoResponse, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponse, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponse, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponse, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponse, GetExtensionsResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponse, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponse, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponse, GetPromptResponses, GetPromptsData, GetPromptsResponse, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponse, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponse, GetProviderCatalogTemplateResponses, GetProviderModelInfoData, GetProviderModelInfoErrors, GetProviderModelInfoResponse, GetProviderModelInfoResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponse, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponse, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponse, GetSessionExtensionsResponses, GetSessionResponse, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponse, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsQuery, GetToolsResponse, GetToolsResponses, GooseApp, GooseMode, HfGgufFile, HfModelInfo, HfModelVariant, HfQuantVariant, Icon, IconTheme, ImageContent, InferenceMetadata, InspectJobResponse, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponse, InspectRunningJobResponses, JsonObject, KillJobResponse, KillRunningJobData, KillRunningJobResponses, ListBuiltinChatTemplatesData, ListBuiltinChatTemplatesResponse, ListBuiltinChatTemplatesResponses, ListLocalModelsData, ListLocalModelsResponse, ListLocalModelsResponses, ListModelsData, ListModelsResponse, ListModelsResponses, ListProviderSecretsData, ListProviderSecretsErrors, ListProviderSecretsResponse, ListProviderSecretsResponses, ListRecipeResponse, ListRecipesData, ListRecipesErrors, ListRecipesResponse, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponse, ListSchedulesResponse2, ListSchedulesResponses, LoadedProvider, LocalModelResponse, McpAppResource, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, Message, MessageContent, MessageEvent, MessageMetadata, ModelCapabilities, ModelConfig, ModelDownloadStatus, ModelInfo, ModelInfoData, ModelInfoQuery, ModelInfoResponse, ModelSettings, ModelTemplate, ParseRecipeData, ParseRecipeError, ParseRecipeErrors, ParseRecipeRequest, ParseRecipeResponse, ParseRecipeResponse2, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponse, PauseScheduleResponses, Permission, PermissionLevel, PermissionsMetadata, PrincipalType, PromptContentResponse, PromptsListResponse, ProviderCatalogEntry, ProviderDetails, ProviderEngine, ProviderMetadata, ProviderModelInfoQuery, ProvidersData, ProviderSecret, ProviderSecretsResponse, ProviderSecretStatus, ProviderSecretStorage, ProvidersResponse, ProvidersResponse2, ProvidersResponses, ProviderTemplate, ProviderType, RawAudioContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ReadAllConfigData, ReadAllConfigResponse, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, Recipe, RecipeManifest, RecipeParameter, RecipeParameterInputType, RecipeParameterRequirement, RecipeToYamlData, RecipeToYamlError, RecipeToYamlErrors, RecipeToYamlRequest, RecipeToYamlResponse, RecipeToYamlResponse2, RecipeToYamlResponses, RedactedThinkingContent, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponse, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponse, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionRequest, RemoveExtensionResponse, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponse, ReplyResponses, RepoVariantsResponse, ResetPromptData, ResetPromptErrors, ResetPromptResponse, ResetPromptResponses, ResourceContents, ResourceMetadata, Response, RestartAgentData, RestartAgentErrors, RestartAgentRequest, RestartAgentResponse, RestartAgentResponse2, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentRequest, ResumeAgentResponse, ResumeAgentResponse2, ResumeAgentResponses, RetryConfig, Role, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponse, RunNowHandlerResponses, RunNowResponse, SamplingConfig, SavePromptData, SavePromptErrors, SavePromptRequest, SavePromptResponse, SavePromptResponses, SaveRecipeData, SaveRecipeError, SaveRecipeErrors, SaveRecipeRequest, SaveRecipeResponse, SaveRecipeResponse2, SaveRecipeResponses, ScanRecipeData, ScanRecipeRequest, ScanRecipeResponse, ScanRecipeResponse2, ScanRecipeResponses, ScheduledJob, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeRequest, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponse, SearchHfModelsResponses, SendTelemetryEventData, SendTelemetryEventResponses, Session, SessionCancelData, SessionCancelResponses, SessionDisplayInfo, SessionEventsData, SessionEventsErrors, SessionEventsResponse, SessionEventsResponses, SessionExtensionsResponse, SessionReplyData, SessionReplyErrors, SessionReplyRequest, SessionReplyResponse, SessionReplyResponse2, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponse, SessionsHandlerResponses, SessionsQuery, SessionType, SetConfigProviderData, SetProviderRequest, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, SetSlashCommandRequest, Settings, SetupResponse, SlashCommand, SlashCommandsResponse, StartAgentData, StartAgentError, StartAgentErrors, StartAgentRequest, StartAgentResponse, StartAgentResponses, StartNanogptSetupData, StartNanogptSetupResponse, StartNanogptSetupResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponse, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponse, StartTetrateSetupResponses, StatusData, StatusResponse, StatusResponses, StopAgentData, StopAgentErrors, StopAgentRequest, StopAgentResponse, StopAgentResponses, SubRecipe, SuccessCheck, SyncFeaturedModelsData, SyncFeaturedModelsResponses, SystemInfo, SystemInfoData, SystemInfoResponse, SystemInfoResponses, SystemNotificationContent, SystemNotificationType, TaskSupport, TelemetryEventRequest, Template, TextContent, ThinkingContent, ThinkingEffort, TokenState, Tool, ToolAnnotations, ToolCallingMode, ToolConfirmationRequest, ToolExecution, ToolInfo, ToolRequest, ToolResponse, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponse, TranscribeDictationResponses, TranscribeRequest, TranscribeResponse, UiMetadata, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponse, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderRequest, UpdateCustomProviderResponse, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionRequest, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponse, UpdateModelSettingsResponses, UpdateProviderRequest, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleRequest, UpdateScheduleResponse, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameRequest, UpdateSessionNameResponses, UpdateSessionRequest, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesError, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesRequest, UpdateSessionUserRecipeValuesResponse, UpdateSessionUserRecipeValuesResponse2, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirRequest, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigQuery, UpsertConfigResponse, UpsertConfigResponses, Usage, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponse, ValidateConfigResponses, WhisperModelResponse, WindowProps } from './types.gen'; diff --git a/ui/desktop/src/api/sdk.gen.ts b/ui/desktop/src/api/sdk.gen.ts index 60b11a114c90..34d0cb896156 100644 --- a/ui/desktop/src/api/sdk.gen.ts +++ b/ui/desktop/src/api/sdk.gen.ts @@ -2,7 +2,7 @@ import type { Client, Options as Options2, TDataShape } from './client'; import { client } from './client.gen'; -import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CheckProviderData, CleanupProviderCacheData, CleanupProviderCacheErrors, CleanupProviderCacheResponses, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteProviderSecretData, DeleteProviderSecretErrors, DeleteProviderSecretResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ForkSessionData, ForkSessionErrors, ForkSessionResponses, GetCanonicalModelInfoData, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponses, GetPromptsData, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponses, GetProviderModelInfoData, GetProviderModelInfoErrors, GetProviderModelInfoResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListBuiltinChatTemplatesData, ListBuiltinChatTemplatesResponses, ListLocalModelsData, ListLocalModelsResponses, ListModelsData, ListModelsResponses, ListProviderSecretsData, ListProviderSecretsErrors, ListProviderSecretsResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, RecipeToYamlData, RecipeToYamlErrors, RecipeToYamlResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResetPromptData, ResetPromptErrors, ResetPromptResponses, RestartAgentData, RestartAgentErrors, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SavePromptData, SavePromptErrors, SavePromptResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponses, SendTelemetryEventData, SendTelemetryEventResponses, SessionCancelData, SessionCancelResponses, SessionEventsData, SessionEventsErrors, SessionEventsResponses, SessionReplyData, SessionReplyErrors, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartNanogptSetupData, StartNanogptSetupResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StatusData, StatusResponses, StopAgentData, StopAgentErrors, StopAgentResponses, SyncFeaturedModelsData, SyncFeaturedModelsResponses, SystemInfoData, SystemInfoResponses, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; +import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CheckProviderData, CleanupProviderCacheData, CleanupProviderCacheErrors, CleanupProviderCacheResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteProviderSecretData, DeleteProviderSecretErrors, DeleteProviderSecretResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ForkSessionData, ForkSessionErrors, ForkSessionResponses, GetCanonicalModelInfoData, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponses, GetPromptsData, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponses, GetProviderModelInfoData, GetProviderModelInfoErrors, GetProviderModelInfoResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListBuiltinChatTemplatesData, ListBuiltinChatTemplatesResponses, ListLocalModelsData, ListLocalModelsResponses, ListModelsData, ListModelsResponses, ListProviderSecretsData, ListProviderSecretsErrors, ListProviderSecretsResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, RecipeToYamlData, RecipeToYamlErrors, RecipeToYamlResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResetPromptData, ResetPromptErrors, ResetPromptResponses, RestartAgentData, RestartAgentErrors, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SavePromptData, SavePromptErrors, SavePromptResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponses, SendTelemetryEventData, SendTelemetryEventResponses, SessionCancelData, SessionCancelResponses, SessionEventsData, SessionEventsErrors, SessionEventsResponses, SessionReplyData, SessionReplyErrors, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartNanogptSetupData, StartNanogptSetupResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StatusData, StatusResponses, StopAgentData, StopAgentErrors, StopAgentResponses, SyncFeaturedModelsData, SyncFeaturedModelsResponses, SystemInfoData, SystemInfoResponses, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; export type Options = Options2 & { /** @@ -174,15 +174,6 @@ export const addExtension = (options: Opti export const removeExtension = (options: Options) => (options.client ?? client).delete({ url: '/config/extensions/{name}', ...options }); -export const upsertPermissions = (options: Options) => (options.client ?? client).post({ - url: '/config/permissions', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } -}); - export const getPrompts = (options?: Options) => (options?.client ?? client).get({ url: '/config/prompts', ...options }); export const resetPrompt = (options: Options) => (options.client ?? client).delete({ url: '/config/prompts/{name}', ...options }); @@ -221,8 +212,6 @@ export const getProviderModelInfo = (optio export const getProviderModels = (options: Options) => (options.client ?? client).get({ url: '/config/providers/{name}/models', ...options }); -export const configureProviderOauth = (options: Options) => (options.client ?? client).post({ url: '/config/providers/{name}/oauth', ...options }); - export const readConfig = (options: Options) => (options.client ?? client).post({ url: '/config/read', ...options, @@ -519,8 +508,3 @@ export const sendTelemetryEvent = (options ...options.headers } }); - -/** - * Get tunnel info - */ -export const getTunnelStatus = (options?: Options) => (options?.client ?? client).get({ url: '/tunnel/status', ...options }); diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index b23177a8bbf2..6575fc7b557d 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -1605,11 +1605,6 @@ export type ToolInfo = { permission?: PermissionLevel | null; }; -export type ToolPermission = { - permission: PermissionLevel; - tool_name: string; -}; - export type ToolRequest = { _meta?: { [key: string]: unknown; @@ -1652,15 +1647,6 @@ export type TranscribeResponse = { text: string; }; -export type TunnelInfo = { - hostname: string; - secret: string; - state: TunnelState; - url: string; -}; - -export type TunnelState = 'idle' | 'starting' | 'running' | 'error' | 'disabled'; - /** * UI-specific metadata for MCP resources */ @@ -1747,10 +1733,6 @@ export type UpsertConfigQuery = { value: unknown; }; -export type UpsertPermissionsQuery = { - tool_permissions: Array; -}; - /** * `input_tokens` is the total input including cache read/write tokens; * the cache fields are breakdown subsets of it. Parsers for providers @@ -2402,29 +2384,6 @@ export type RemoveExtensionResponses = { export type RemoveExtensionResponse = RemoveExtensionResponses[keyof RemoveExtensionResponses]; -export type UpsertPermissionsData = { - body: UpsertPermissionsQuery; - path?: never; - query?: never; - url: '/config/permissions'; -}; - -export type UpsertPermissionsErrors = { - /** - * Invalid request - */ - 400: unknown; -}; - -export type UpsertPermissionsResponses = { - /** - * Permission update completed - */ - 200: string; -}; - -export type UpsertPermissionsResponse = UpsertPermissionsResponses[keyof UpsertPermissionsResponses]; - export type GetPromptsData = { body?: never; path?: never; @@ -2760,32 +2719,6 @@ export type GetProviderModelsResponses = { export type GetProviderModelsResponse = GetProviderModelsResponses[keyof GetProviderModelsResponses]; -export type ConfigureProviderOauthData = { - body?: never; - path: { - /** - * Provider name - */ - name: string; - }; - query?: never; - url: '/config/providers/{name}/oauth'; -}; - -export type ConfigureProviderOauthErrors = { - /** - * OAuth configuration failed - */ - 400: unknown; -}; - -export type ConfigureProviderOauthResponses = { - /** - * OAuth configuration completed - */ - 200: unknown; -}; - export type ReadConfigData = { body: ConfigKeyQuery; path?: never; @@ -4340,19 +4273,3 @@ export type SendTelemetryEventResponses = { */ 202: unknown; }; - -export type GetTunnelStatusData = { - body?: never; - path?: never; - query?: never; - url: '/tunnel/status'; -}; - -export type GetTunnelStatusResponses = { - /** - * Tunnel info - */ - 200: TunnelInfo; -}; - -export type GetTunnelStatusResponse = GetTunnelStatusResponses[keyof GetTunnelStatusResponses]; diff --git a/ui/desktop/src/components/ChatInput.tsx b/ui/desktop/src/components/ChatInput.tsx index 464ecfd2798c..25dec3d58fbe 100644 --- a/ui/desktop/src/components/ChatInput.tsx +++ b/ui/desktop/src/components/ChatInput.tsx @@ -14,8 +14,8 @@ import ModelsBottomBar from './settings/models/bottom_bar/ModelsBottomBar'; import { BottomMenuExtensionSelection } from './bottom_menu/BottomMenuExtensionSelection'; import { cn } from '../utils'; import { AlertType, useAlerts } from './alerts'; -import { useConfig } from './ConfigContext'; import { useModelAndProvider } from './ModelAndProviderContext'; +import { acpListProviderDetails } from '../acp/providers'; import { USE_ACP_CHAT } from '../acpChatFeatureFlag'; import { useAudioRecorder } from '../hooks/useAudioRecorder'; import { toastError } from '../toasts'; @@ -286,7 +286,6 @@ export default function ChatInput({ null ) as React.RefObject; const intl = useIntl(); - const { getProviders } = useConfig(); const { getCurrentModelAndProvider, currentModel: configModel, @@ -609,14 +608,14 @@ export default function ChatInput({ // Priority 2: Check canonical model info (source of truth) const canonicalInfo = await fetchCanonicalModelInfo(provider, model); - if (canonicalInfo?.context_limit) { - setTokenLimit(canonicalInfo.context_limit); + if (canonicalInfo?.contextLimit) { + setTokenLimit(canonicalInfo.contextLimit); setIsTokenLimitLoaded(true); return; } // Priority 3: Fall back to provider metadata known_models (may be outdated) - const providers = await getProviders(true); + const providers = await acpListProviderDetails(); const currentProvider = providers.find((p) => p.name === provider); if (currentProvider?.metadata?.known_models) { const modelConfig = currentProvider.metadata.known_models.find((m) => m.name === model); diff --git a/ui/desktop/src/components/ConfigContext.tsx b/ui/desktop/src/components/ConfigContext.tsx index 834c3d6c005a..2672a397f104 100644 --- a/ui/desktop/src/components/ConfigContext.tsx +++ b/ui/desktop/src/components/ConfigContext.tsx @@ -1,5 +1,6 @@ import React, { createContext, useContext, useState, useEffect, useMemo, useCallback } from 'react'; -import { readAllConfig, readConfig, removeConfig, upsertConfig, providers } from '../api'; +import { acpReadAllConfig, acpReadConfig, acpRemoveConfig, acpUpsertConfig } from '../acp/config'; +import { acpListProviderDetails } from '../acp/providers'; import { getConfiguredExtensions, addConfigExtension, @@ -8,13 +9,7 @@ import { } from '../acp/extensions'; import { pruneDeprecatedBundledExtensions, syncBundledExtensions } from './settings/extensions'; import { nameToKey } from './settings/extensions/utils'; -import type { - ConfigResponse, - UpsertConfigQuery, - ConfigKeyQuery, - ProviderDetails, - ExtensionConfig, -} from '../api'; +import type { ConfigResponse, ProviderDetails, ExtensionConfig } from '../api'; export type { ExtensionConfig } from '../api/types.gen'; @@ -56,20 +51,13 @@ export const ConfigProvider: React.FC = ({ children }) => { providersListRef.current = providersList; const reloadConfig = useCallback(async () => { - const response = await readAllConfig(); - setConfig(response.data?.config || {}); + const config = await acpReadAllConfig(); + setConfig(config); }, []); const upsert = useCallback( async (key: string, value: unknown, isSecret: boolean = false) => { - const query: UpsertConfigQuery = { - key: key, - value: value, - is_secret: isSecret, - }; - await upsertConfig({ - body: query, - }); + await acpUpsertConfig(key, value, isSecret); await reloadConfig(); }, [reloadConfig] @@ -77,24 +65,21 @@ export const ConfigProvider: React.FC = ({ children }) => { const read = useCallback( async (key: string, is_secret: boolean = false, options?: { throwOnError?: boolean }) => { - const query: ConfigKeyQuery = { key: key, is_secret: is_secret }; - const response = await readConfig({ - body: query, - }); - if (options?.throwOnError && response.error) { - throw response.error; + try { + return await acpReadConfig(key, is_secret); + } catch (error) { + if (options?.throwOnError) { + throw error; + } + return null; } - return response.data; }, [] ); const remove = useCallback( async (key: string, is_secret: boolean) => { - const query: ConfigKeyQuery = { key: key, is_secret: is_secret }; - await removeConfig({ - body: query, - }); + await acpRemoveConfig(key, is_secret); await reloadConfig(); }, [reloadConfig] @@ -150,8 +135,7 @@ export const ConfigProvider: React.FC = ({ children }) => { const getProviders = useCallback(async (forceRefresh = false): Promise => { if (forceRefresh || providersListRef.current.length === 0) { try { - const response = await providers(); - const providersData = response.data || []; + const providersData = await acpListProviderDetails(); providersListRef.current = providersData; setProvidersList(providersData); return providersData; @@ -167,13 +151,12 @@ export const ConfigProvider: React.FC = ({ children }) => { // Load all configuration data and providers on mount (async () => { // Load config - const configResponse = await readAllConfig(); - setConfig(configResponse.data?.config || {}); + const configResponse = await acpReadAllConfig(); + setConfig(configResponse); // Load providers try { - const providersResponse = await providers(); - const providersData = providersResponse.data || []; + const providersData = await acpListProviderDetails(); providersListRef.current = providersData; setProvidersList(providersData); } catch (error) { diff --git a/ui/desktop/src/components/ModelAndProviderContext.tsx b/ui/desktop/src/components/ModelAndProviderContext.tsx index 8b5f19d6249c..077c3b349567 100644 --- a/ui/desktop/src/components/ModelAndProviderContext.tsx +++ b/ui/desktop/src/components/ModelAndProviderContext.tsx @@ -1,8 +1,8 @@ import React, { createContext, useContext, useState, useEffect, useMemo, useCallback } from 'react'; import { toastError, toastSuccess } from '../toasts'; import Model, { getProviderMetadata } from './settings/models/modelInterface'; -import { ProviderMetadata, setConfigProvider, updateAgentProvider } from '../api'; -import { useConfig } from './ConfigContext'; +import { ProviderMetadata } from '../api'; +import { acpReadDefaults, acpSaveDefaults, acpSetSessionProviderModel } from '../acp/providers'; import { errorMessage } from '../utils/conversionUtils'; import { getModelDisplayName, @@ -60,7 +60,6 @@ export { i18n as modelAndProviderMessages }; export const ModelAndProviderProvider: React.FC = ({ children }) => { const [currentModel, setCurrentModel] = useState(null); const [currentProvider, setCurrentProvider] = useState(null); - const { read, getProviders } = useConfig(); const intl = useIntl(); const changeModel = useCallback( @@ -71,31 +70,14 @@ export const ModelAndProviderProvider: React.FC = try { if (sessionId) { - const response = await updateAgentProvider({ - body: { - session_id: sessionId, - provider: providerName, - model: modelName, - context_limit: model.context_limit, - request_params: model.request_params, - }, - }); - if (response.error) { - throw new Error(`Failed to update agent provider: ${response.error}`); - } + await acpSetSessionProviderModel(sessionId, providerName, modelName); } // Only update the global config default when there's no session // (i.e. changing from settings, not from within an existing chat) if (!sessionId) { phase = 'config'; - await setConfigProvider({ - body: { - provider: providerName, - model: modelName, - }, - throwOnError: true, - }); + await acpSaveDefaults(providerName, modelName); } if (!sessionId) { @@ -132,13 +114,7 @@ export const ModelAndProviderProvider: React.FC = const model = window.appConfig.get('GOOSE_DEFAULT_MODEL') as string; if (provider && model) { try { - await setConfigProvider({ - body: { - provider: provider, - model: model, - }, - throwOnError: true, - }); + await acpSaveDefaults(provider, model); } catch (error) { console.error('[getFallbackModelAndProvider] Failed to write to config', error); } @@ -147,22 +123,22 @@ export const ModelAndProviderProvider: React.FC = }, []); const getCurrentModelAndProvider = useCallback(async () => { - let model: string; - let provider: string; + let model: string | null; + let provider: string | null; - // read from config try { - model = (await read('GOOSE_MODEL', false)) as string; - provider = (await read('GOOSE_PROVIDER', false)) as string; + const defaults = await acpReadDefaults(); + model = defaults.modelId; + provider = defaults.providerId; } catch { - console.error(`Failed to read GOOSE_MODEL or GOOSE_PROVIDER from config`); - throw new Error('Failed to read GOOSE_MODEL or GOOSE_PROVIDER from config'); + console.error(`Failed to read default model or provider`); + throw new Error('Failed to read default model or provider'); } if (!model || !provider) { return getFallbackModelAndProvider(); } return { model: model, provider: provider }; - }, [read, getFallbackModelAndProvider]); + }, [getFallbackModelAndProvider]); const getCurrentModelAndProviderForDisplay = useCallback(async () => { const modelProvider = await getCurrentModelAndProvider(); @@ -173,28 +149,28 @@ export const ModelAndProviderProvider: React.FC = let metadata: ProviderMetadata; try { - metadata = await getProviderMetadata(String(gooseProvider), getProviders); + metadata = await getProviderMetadata(String(gooseProvider)); } catch { return { model: gooseModel, provider: gooseProvider }; } const providerDisplayName = metadata.display_name; return { model: gooseModel, provider: providerDisplayName }; - }, [getCurrentModelAndProvider, getProviders]); + }, [getCurrentModelAndProvider]); const getCurrentModelDisplayName = useCallback(async () => { try { - const currentModelName = (await read('GOOSE_MODEL', false)) as string; - return getModelDisplayName(currentModelName); + const { modelId } = await acpReadDefaults(); + return getModelDisplayName(modelId ?? ''); } catch { return intl.formatMessage(i18n.selectModel); } - }, [read, intl]); + }, [intl]); const getCurrentProviderDisplayName = useCallback(async () => { try { - const currentModelName = (await read('GOOSE_MODEL', false)) as string; - const providerDisplayName = getProviderDisplayName(currentModelName); + const { modelId } = await acpReadDefaults(); + const providerDisplayName = getProviderDisplayName(modelId ?? ''); if (providerDisplayName) { return providerDisplayName; } @@ -204,7 +180,7 @@ export const ModelAndProviderProvider: React.FC = } catch { return ''; } - }, [read, getCurrentModelAndProviderForDisplay]); + }, [getCurrentModelAndProviderForDisplay]); const refreshCurrentModelAndProvider = useCallback(async () => { try { diff --git a/ui/desktop/src/components/ToolApprovalButtons.test.tsx b/ui/desktop/src/components/ToolApprovalButtons.test.tsx index c7a9890c3a5d..6800c6fb5917 100644 --- a/ui/desktop/src/components/ToolApprovalButtons.test.tsx +++ b/ui/desktop/src/components/ToolApprovalButtons.test.tsx @@ -6,6 +6,10 @@ import { resolveAcpPermissionRequest } from '../acp/permissionRequests'; import { IntlTestWrapper } from '../i18n/test-utils'; import ToolApprovalButtons from './ToolApprovalButtons'; +const acpChatFeatureFlagMock = vi.hoisted(() => ({ + useAcpChat: true, +})); + vi.mock('../api', () => ({ confirmToolAction: vi.fn(), })); @@ -15,7 +19,9 @@ vi.mock('../acp/permissionRequests', () => ({ })); vi.mock('../acpChatFeatureFlag', () => ({ - USE_ACP_CHAT: true, + get USE_ACP_CHAT() { + return acpChatFeatureFlagMock.useAcpChat; + }, })); const renderWithIntl = (ui: React.ReactElement, options?: RenderOptions) => @@ -27,6 +33,7 @@ const resolveAcpPermissionRequestMock = vi.mocked(resolveAcpPermissionRequest); describe('ToolApprovalButtons', () => { beforeEach(() => { vi.clearAllMocks(); + acpChatFeatureFlagMock.useAcpChat = true; }); it('marks the approval accepted when the ACP request resolves', async () => { @@ -53,11 +60,8 @@ describe('ToolApprovalButtons', () => { expect(screen.getByText('developer__shell - Allowed once')).toBeInTheDocument(); }); - it('falls back to the REST confirmation when no ACP request is pending', async () => { + it('shows a stale request error when ACP has no pending request', async () => { resolveAcpPermissionRequestMock.mockReturnValueOnce(false); - confirmToolActionMock.mockResolvedValueOnce({ error: undefined } as Awaited< - ReturnType - >); renderWithIntl( { 'tool-call-rerun', 'allow_once' ); + expect(confirmToolActionMock).not.toHaveBeenCalled(); + expect(screen.getByText('This approval request is no longer active.')).toBeInTheDocument(); + expect(screen.queryByText('developer__shell - Allowed once')).not.toBeInTheDocument(); + }); + + it('uses the REST confirmation path when ACP chat is disabled', async () => { + acpChatFeatureFlagMock.useAcpChat = false; + confirmToolActionMock.mockResolvedValueOnce({ error: undefined } as Awaited< + ReturnType + >); + + renderWithIntl( + + ); + + await userEvent.click(screen.getByRole('button', { name: 'Allow Once' })); + + expect(resolveAcpPermissionRequestMock).not.toHaveBeenCalled(); expect(confirmToolActionMock).toHaveBeenCalledWith({ body: { sessionId: 'session-1', - id: 'tool-call-rerun', + id: 'tool-call-rest', action: 'allow_once', principalType: 'Tool', }, diff --git a/ui/desktop/src/components/ToolApprovalButtons.tsx b/ui/desktop/src/components/ToolApprovalButtons.tsx index e6472c0b690e..8922611bad06 100644 --- a/ui/desktop/src/components/ToolApprovalButtons.tsx +++ b/ui/desktop/src/components/ToolApprovalButtons.tsx @@ -38,6 +38,10 @@ const i18n = defineMessages({ id: 'toolApprovalButtons.cancelled', defaultMessage: 'Cancelled', }, + staleApprovalRequest: { + id: 'toolApprovalButtons.staleApprovalRequest', + defaultMessage: 'This approval request is no longer active.', + }, }); const globalApprovalState = new Map< @@ -63,6 +67,13 @@ export default function ToolApprovalButtons({ data }: { data: ToolApprovalData } const storedState = globalApprovalState.get(id); const [decision, setDecision] = useState(storedState?.decision ?? null); const [isClicked, setIsClicked] = useState(storedState?.isClicked ?? initialIsClicked ?? false); + const [approvalError, setApprovalError] = useState(null); + + const setResolvedDecision = (action: Permission) => { + setDecision(action); + setIsClicked(true); + setApprovalError(null); + }; useEffect(() => { const currentState = globalApprovalState.get(id); @@ -70,6 +81,7 @@ export default function ToolApprovalButtons({ data }: { data: ToolApprovalData } setDecision(currentState.decision); setIsClicked(currentState.isClicked); } + setApprovalError(null); }, [id]); useEffect(() => { @@ -78,16 +90,16 @@ export default function ToolApprovalButtons({ data }: { data: ToolApprovalData } const handleAction = async (action: Permission) => { try { - // Edit-in-place reruns go through the legacy REST path even when ACP chat is - // enabled, so fall back to confirmToolAction when no ACP request is pending. - if (USE_ACP_CHAT && resolveAcpPermissionRequest(sessionId, id, action)) { - setDecision(action); - setIsClicked(true); + if (USE_ACP_CHAT) { + if (resolveAcpPermissionRequest(sessionId, id, action)) { + setResolvedDecision(action); + } else { + setApprovalError(intl.formatMessage(i18n.staleApprovalRequest)); + } return; } - setDecision(action); - setIsClicked(true); + setResolvedDecision(action); const response = await confirmToolAction({ body: { @@ -121,26 +133,33 @@ export default function ToolApprovalButtons({ data }: { data: ToolApprovalData } } return ( -
- - {!prompt && ( + <> +
+ {!prompt && ( + + )} + +
+ {approvalError && ( +

+ {approvalError} +

)} - -
+ ); } diff --git a/ui/desktop/src/components/__tests__/ParameterInputModal.test.tsx b/ui/desktop/src/components/__tests__/ParameterInputModal.test.tsx index 802dbaccdd45..68dee40f11ff 100644 --- a/ui/desktop/src/components/__tests__/ParameterInputModal.test.tsx +++ b/ui/desktop/src/components/__tests__/ParameterInputModal.test.tsx @@ -1,26 +1,22 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, type RenderOptions, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { MemoryRouter, useLocation } from 'react-router-dom'; import ParameterInputModal from '../ParameterInputModal'; import { IntlTestWrapper } from '../../i18n/test-utils'; import type { Parameter } from '../../recipe'; -const LocationDisplay = () => { - const location = useLocation(); - return
{location.pathname}
; -}; +const mockNavigate = vi.hoisted(() => vi.fn()); + +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useNavigate: () => mockNavigate, + }; +}); const renderWithIntl = (ui: React.ReactElement, options?: RenderOptions) => - render( - - - {ui} - - - , - options - ); + render(ui, { wrapper: IntlTestWrapper, ...options }); const mockParameters: Parameter[] = [ { @@ -56,6 +52,7 @@ describe('ParameterInputModal', () => { beforeEach(() => { vi.clearAllMocks(); + mockNavigate.mockClear(); }); describe('Rendering', () => { @@ -155,14 +152,14 @@ describe('ParameterInputModal', () => { expect(defaultProps.onClose).toHaveBeenCalled(); }); - it('calls onClose when "Start New Chat" option is selected', async () => { + it('navigates to chat when "Start New Chat" option is selected', async () => { const user = userEvent.setup(); renderWithIntl(); await user.click(screen.getByText('Cancel')); await user.click(screen.getByText('Start New Chat (No Workflow)')); - expect(screen.getByTestId('location-display')).toHaveTextContent('/pair'); + expect(mockNavigate).toHaveBeenCalledWith('/pair'); expect(defaultProps.onClose).not.toHaveBeenCalled(); }); diff --git a/ui/desktop/src/components/alerts/AlertBox.tsx b/ui/desktop/src/components/alerts/AlertBox.tsx index 3e987d3ad761..8d45b945666d 100644 --- a/ui/desktop/src/components/alerts/AlertBox.tsx +++ b/ui/desktop/src/components/alerts/AlertBox.tsx @@ -4,7 +4,6 @@ import { FaPencilAlt, FaSave } from 'react-icons/fa'; import { cn } from '../../utils'; import { errorMessage } from '../../utils/conversionUtils'; import { Alert, AlertType } from './types'; -import { upsertConfig } from '../../api'; import { useConfig } from '../ConfigContext'; import { defineMessages, useIntl } from '../../i18n'; @@ -43,7 +42,7 @@ const alertStyles: Record = { export const AlertBox = ({ alert, className }: AlertBoxProps) => { const intl = useIntl(); - const { read } = useConfig(); + const { read, upsert } = useConfig(); const [isEditingThreshold, setIsEditingThreshold] = useState(false); const [loadedThreshold, setLoadedThreshold] = useState(0.8); const [thresholdValue, setThresholdValue] = useState(80); @@ -79,13 +78,7 @@ export const AlertBox = ({ alert, className }: AlertBoxProps) => { try { const newThreshold = validThreshold / 100; // Convert percentage to decimal - await upsertConfig({ - body: { - key: 'GOOSE_AUTO_COMPACT_THRESHOLD', - value: newThreshold, - is_secret: false, - }, - }); + await upsert('GOOSE_AUTO_COMPACT_THRESHOLD', newThreshold, false); setIsEditingThreshold(false); setLoadedThreshold(newThreshold); diff --git a/ui/desktop/src/components/bottom_menu/CostTracker.tsx b/ui/desktop/src/components/bottom_menu/CostTracker.tsx index 323beb3ee4d6..bbfd9ff235d4 100644 --- a/ui/desktop/src/components/bottom_menu/CostTracker.tsx +++ b/ui/desktop/src/components/bottom_menu/CostTracker.tsx @@ -1,8 +1,7 @@ import { useState, useEffect } from 'react'; import { CoinIcon } from '../icons'; import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/Tooltip'; -import { fetchCanonicalModelInfo } from '../../utils/canonical'; -import type { ModelInfoData } from '../../api'; +import { fetchCanonicalModelInfo, type CanonicalModelInfo } from '../../utils/canonical'; import { defineMessages, useIntl } from '../../i18n'; const i18n = defineMessages({ @@ -40,7 +39,7 @@ export function CostTracker({ provider: currentProvider, }: CostTrackerProps) { const intl = useIntl(); - const [costInfo, setCostInfo] = useState(null); + const [costInfo, setCostInfo] = useState(null); const [isLoading, setIsLoading] = useState(true); const [showPricing, setShowPricing] = useState(true); const [pricingFailed, setPricingFailed] = useState(false); @@ -118,7 +117,7 @@ export function CostTracker({ if ( accumulatedCost == null && (!costInfo || - (costInfo.input_token_cost === undefined && costInfo.output_token_cost === undefined)) + (costInfo.inputTokenCost === undefined && costInfo.outputTokenCost === undefined)) ) { const freeProviders = ['ollama', 'local', 'localhost']; if (freeProviders.includes(currentProvider.toLowerCase())) { @@ -170,14 +169,14 @@ export function CostTracker({ return intl.formatMessage(i18n.totalSessionCost, { cost: `${currency}${totalCost.toFixed(4)}` }) + `\n` + intl.formatMessage(i18n.inputOutputTooltip, { inputTokens: inputTokens.toLocaleString(), - inputCost: `${currency}${((inputTokens * (costInfo?.input_token_cost || 0)) / 1_000_000).toFixed(6)}`, + inputCost: `${currency}${((inputTokens * (costInfo?.inputTokenCost || 0)) / 1_000_000).toFixed(6)}`, outputTokens: outputTokens.toLocaleString(), - outputCost: `${currency}${((outputTokens * (costInfo?.output_token_cost || 0)) / 1_000_000).toFixed(6)}`, + outputCost: `${currency}${((outputTokens * (costInfo?.outputTokenCost || 0)) / 1_000_000).toFixed(6)}`, }); } - const inputCostStr = `${currency}${((inputTokens * (costInfo?.input_token_cost || 0)) / 1_000_000).toFixed(6)}`; - const outputCostStr = `${currency}${((outputTokens * (costInfo?.output_token_cost || 0)) / 1_000_000).toFixed(6)}`; + const inputCostStr = `${currency}${((inputTokens * (costInfo?.inputTokenCost || 0)) / 1_000_000).toFixed(6)}`; + const outputCostStr = `${currency}${((outputTokens * (costInfo?.outputTokenCost || 0)) / 1_000_000).toFixed(6)}`; return intl.formatMessage(i18n.inputOutputTooltip, { inputTokens: inputTokens.toLocaleString(), inputCost: inputCostStr, diff --git a/ui/desktop/src/components/onboarding/OnboardingGuard.tsx b/ui/desktop/src/components/onboarding/OnboardingGuard.tsx index dab4439497b5..48b4b5b260b0 100644 --- a/ui/desktop/src/components/onboarding/OnboardingGuard.tsx +++ b/ui/desktop/src/components/onboarding/OnboardingGuard.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useConfig } from '../ConfigContext'; import { useModelAndProvider } from '../ModelAndProviderContext'; +import { acpListProviderDetails, acpReadDefaults, acpSaveDefaults } from '../../acp/providers'; import apeCloudLogo from '../../images/logo.png'; import { Button } from '../ui/button'; import ProviderSelector from './ProviderSelector'; @@ -18,7 +19,7 @@ import { defineMessages, useIntl } from '../../i18n'; const i18n = defineMessages({ welcomeTitle: { id: 'onboardingGuard.welcomeTitle', - defaultMessage: "Welcome to ApeMind Agent", + defaultMessage: 'Welcome to ApeMind Agent', }, welcomeDescription: { id: 'onboardingGuard.welcomeDescription', @@ -26,7 +27,7 @@ const i18n = defineMessages({ }, checkProviderErrorTitle: { id: 'onboardingGuard.checkProviderErrorTitle', - defaultMessage: "Unable to connect to ApeMind Agent server", + defaultMessage: 'Unable to connect to ApeMind Agent server', }, checkProviderErrorDescription: { id: 'onboardingGuard.checkProviderErrorDescription', @@ -47,7 +48,7 @@ interface OnboardingGuardProps { export default function OnboardingGuard({ children }: OnboardingGuardProps) { const intl = useIntl(); const navigate = useNavigate(); - const { read, upsert, getProviders } = useConfig(); + const { upsert } = useConfig(); const { getFallbackModelAndProvider, refreshCurrentModelAndProvider } = useModelAndProvider(); const [isCheckingProvider, setIsCheckingProvider] = useState(true); @@ -66,7 +67,7 @@ export default function OnboardingGuard({ children }: OnboardingGuardProps) { setCheckProviderError(false); for (let attempt = 0; attempt <= retries; attempt++) { try { - const provider = (await read('GOOSE_PROVIDER', false, { throwOnError: true })) as string | null; + const { providerId: provider } = await acpReadDefaults(); if (provider?.trim()) { setHasProvider(true); setIsCheckingProvider(false); @@ -75,8 +76,8 @@ export default function OnboardingGuard({ children }: OnboardingGuardProps) { const fallback = await getFallbackModelAndProvider(); if (fallback.provider?.trim() && fallback.model?.trim()) { - const configuredProvider = (await read('GOOSE_PROVIDER', false)) as string | null; - const configuredModel = (await read('GOOSE_MODEL', false)) as string | null; + const { providerId: configuredProvider, modelId: configuredModel } = + await acpReadDefaults(); if (configuredProvider?.trim() && configuredModel?.trim()) { await refreshCurrentModelAndProvider(); setHasProvider(true); @@ -113,16 +114,11 @@ export default function OnboardingGuard({ children }: OnboardingGuardProps) { const handleConfigured = async (providerName: string, modelId?: string) => { trackOnboardingProviderSelected({ provider: providerName }); - await upsert('GOOSE_PROVIDER', providerName, false); - const providers = await getProviders(true); + const providers = await acpListProviderDetails(); const matchedProvider = providers.find((p) => p.name === providerName); - if (modelId) { - await upsert('GOOSE_MODEL', modelId, false); - setConfiguredModel(modelId); - } else if (matchedProvider) { - await upsert('GOOSE_MODEL', matchedProvider.metadata.default_model, false); - setConfiguredModel(matchedProvider.metadata.default_model); - } + const resolvedModel = modelId ?? matchedProvider?.metadata.default_model ?? null; + await acpSaveDefaults(providerName, resolvedModel); + setConfiguredModel(resolvedModel); await refreshCurrentModelAndProvider(); setConfiguredProvider(providerName); setConfiguredProviderDisplayName(matchedProvider?.metadata.display_name || providerName); diff --git a/ui/desktop/src/components/onboarding/ProviderConfigForm.tsx b/ui/desktop/src/components/onboarding/ProviderConfigForm.tsx index ebf1acd6b730..dde92a8a5005 100644 --- a/ui/desktop/src/components/onboarding/ProviderConfigForm.tsx +++ b/ui/desktop/src/components/onboarding/ProviderConfigForm.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { configureProviderOauth, ProviderDetails } from '../../api'; -import { useConfig } from '../ConfigContext'; +import { ProviderDetails } from '../../api'; +import { acpAuthenticateProvider } from '../../acp/providers'; import DefaultProviderSetupForm, { ConfigInput, } from '../settings/providers/modal/subcomponents/forms/DefaultProviderSetupForm'; @@ -78,10 +78,7 @@ function OAuthForm({ const handleLogin = async () => { setIsLoading(true); try { - await configureProviderOauth({ - path: { name: provider.name }, - throwOnError: true, - }); + await acpAuthenticateProvider(provider.name); onConfigured(provider.name); } catch (err) { onError(`Sign-in failed: ${err instanceof Error ? err.message : String(err)}`); @@ -101,7 +98,9 @@ function OAuthForm({ size="lg" > - {isLoading ? intl.formatMessage(i18n.signingIn) : intl.formatMessage(i18n.signInWith, { providerName: provider.metadata.display_name })} + {isLoading + ? intl.formatMessage(i18n.signingIn) + : intl.formatMessage(i18n.signInWith, { providerName: provider.metadata.display_name })}

{isDeviceCodeFlow @@ -122,7 +121,6 @@ function ApiKeyForm({ onError: (msg: string) => void; }) { const intl = useIntl(); - const { upsert } = useConfig(); const [configValues, setConfigValues] = useState>({}); const [validationErrors, setValidationErrors] = useState>({}); const [isSubmitting, setIsSubmitting] = useState(false); @@ -158,7 +156,7 @@ function ApiKeyForm({ setIsSubmitting(true); try { - await providerConfigSubmitHandler(upsert, provider, toSubmit); + await providerConfigSubmitHandler(provider, toSubmit); onConfigured(provider.name); } catch (err) { const msg = diff --git a/ui/desktop/src/components/onboarding/ProviderSelector.tsx b/ui/desktop/src/components/onboarding/ProviderSelector.tsx index ceb58ffc9e44..9cb222d57fac 100644 --- a/ui/desktop/src/components/onboarding/ProviderSelector.tsx +++ b/ui/desktop/src/components/onboarding/ProviderSelector.tsx @@ -1,10 +1,9 @@ import { useState, useEffect, useMemo } from 'react'; +import { ProviderDetails, UpdateCustomProviderRequest } from '../../api'; import { - providers as fetchProviders, - createCustomProvider, - ProviderDetails, - UpdateCustomProviderRequest, -} from '../../api'; + acpCreateCustomProviderFromRequest, + acpListProviderDetails, +} from '../../acp/providers'; import { Select } from '../ui/Select'; import ProviderConfigForm from './ProviderConfigForm'; import FreeOptionCards from './FreeOptionCards'; @@ -73,13 +72,8 @@ export default function ProviderSelector({ useEffect(() => { const load = async () => { try { - const response = await fetchProviders({ throwOnError: true }); - if (response.data) { - const list = Array.isArray(response.data) - ? response.data - : (response.data as { providers: ProviderDetails[] }).providers || []; - setProviderList(list); - } + const list = await acpListProviderDetails(); + setProviderList(list); } catch (err) { console.error('Failed to fetch providers:', err); } @@ -127,10 +121,10 @@ export default function ProviderSelector({ }; const handleCreateCustomProvider = async (data: UpdateCustomProviderRequest) => { - const result = await createCustomProvider({ body: data, throwOnError: true }); + const result = await acpCreateCustomProviderFromRequest(data); setShowCustomModal(false); - if (result.data?.provider_name) { - onConfigured(result.data.provider_name); + if (result.provider_name) { + onConfigured(result.provider_name); } }; diff --git a/ui/desktop/src/components/recipes/shared/RecipeModelSelector.tsx b/ui/desktop/src/components/recipes/shared/RecipeModelSelector.tsx index ad616bfdb389..9a9c3589bec8 100644 --- a/ui/desktop/src/components/recipes/shared/RecipeModelSelector.tsx +++ b/ui/desktop/src/components/recipes/shared/RecipeModelSelector.tsx @@ -1,7 +1,7 @@ import { useEffect, useState, useCallback } from 'react'; import { Select } from '../../ui/Select'; import { Input } from '../../ui/input'; -import { useConfig } from '../../ConfigContext'; +import { acpListProviderDetails } from '../../../acp/providers'; import { fetchModelsForProviders } from '../../settings/models/modelInterface'; import { defineMessages, useIntl } from '../../../i18n'; @@ -70,7 +70,6 @@ export const RecipeModelSelector = ({ onModelChange, }: RecipeModelSelectorProps) => { const intl = useIntl(); - const { getProviders } = useConfig(); const [providerOptions, setProviderOptions] = useState<{ value: string; label: string }[]>([]); const [modelOptions, setModelOptions] = useState< { options: { value: string; label: string; provider: string }[] }[] @@ -83,7 +82,7 @@ export const RecipeModelSelector = ({ (async () => { try { setFetchError(null); - const providersResponse = await getProviders(false); + const providersResponse = await acpListProviderDetails(); const activeProviders = providersResponse.filter((provider) => provider.is_configured); setProviderOptions([ @@ -132,7 +131,7 @@ export const RecipeModelSelector = ({ setLoadingModels(false); } })(); - }, [getProviders, intl]); + }, [intl]); useEffect(() => { if (!loadingModels && selectedModel && selectedProvider) { diff --git a/ui/desktop/src/components/sessions/SessionListView.tsx b/ui/desktop/src/components/sessions/SessionListView.tsx index 9a53a8568708..c884baa8c1dd 100644 --- a/ui/desktop/src/components/sessions/SessionListView.tsx +++ b/ui/desktop/src/components/sessions/SessionListView.tsx @@ -34,7 +34,6 @@ import { DialogHeader, DialogTitle, } from '../ui/dialog'; -import { getTunnelStatus } from '../../api/sdk.gen'; import { acpDeleteSession, acpExportSession, @@ -384,15 +383,12 @@ const SessionListView: React.FC = React.memo( }; }, [loadSessions, debouncedSearchTerm]); - // Hide Nostr sharing when tunnel is disabled (restricted/enterprise bundles) + // Hide Nostr sharing when explicitly disabled via env var (restricted/enterprise bundles) useEffect(() => { - getTunnelStatus() - .then(({ data }) => { - if (data?.state === 'disabled') { - setNostrEnabled(false); - } - }) - .catch(() => {}); + const config = window.electron.getConfig(); + if (config.GOOSE_DISABLE_NOSTR_SHARING === true) { + setNostrEnabled(false); + } }, []); // Timing logic to prevent flicker between skeleton and content on initial load diff --git a/ui/desktop/src/components/settings/PromptsSettingsSection.tsx b/ui/desktop/src/components/settings/PromptsSettingsSection.tsx index 03600e8c9c0a..dee3a0171eb2 100644 --- a/ui/desktop/src/components/settings/PromptsSettingsSection.tsx +++ b/ui/desktop/src/components/settings/PromptsSettingsSection.tsx @@ -1,12 +1,12 @@ import { useState, useEffect, useCallback } from 'react'; import { - getPrompt, - getPrompts, - PromptContentResponse, - Template, - resetPrompt, - savePrompt, -} from '../../api'; + acpGetPrompt, + acpListPrompts, + acpResetPrompt, + acpSavePrompt, + type PromptContent, + type PromptTemplate, +} from '../../acp/prompts'; import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; import { Button } from '../ui/button'; import { AlertTriangle, RotateCcw, ArrowLeft } from 'lucide-react'; @@ -24,7 +24,8 @@ const i18n = defineMessages({ }, confirmResetAll: { id: 'promptsSettings.confirmResetAll', - defaultMessage: 'Are you sure you want to reset all prompts to their defaults? This cannot be undone.', + defaultMessage: + 'Are you sure you want to reset all prompts to their defaults? This cannot be undone.', }, allPromptsReset: { id: 'promptsSettings.allPromptsReset', @@ -44,7 +45,8 @@ const i18n = defineMessages({ }, confirmResetOne: { id: 'promptsSettings.confirmResetOne', - defaultMessage: 'Are you sure you want to reset this prompt to its default? This cannot be undone.', + defaultMessage: + 'Are you sure you want to reset this prompt to its default? This cannot be undone.', }, promptResetToDefault: { id: 'promptsSettings.promptResetToDefault', @@ -84,7 +86,8 @@ const i18n = defineMessages({ }, templateTip: { id: 'promptsSettings.templateTip', - defaultMessage: 'Template variables like {extensionsExample} or {forExample} are replaced with actual values at runtime. Be careful not to remove required variables.', + defaultMessage: + 'Template variables like {extensionsExample} or {forExample} are replaced with actual values at runtime. Be careful not to remove required variables.', }, editingLabel: { id: 'promptsSettings.editingLabel', @@ -108,7 +111,8 @@ const i18n = defineMessages({ }, promptEditingDescription: { id: 'promptsSettings.promptEditingDescription', - defaultMessage: "Customize the prompts that define ApeMind Agent's behavior in different contexts. These prompts use Jinja2 templating syntax. Be careful when modifying template variables, as incorrect changes can break functionality.", + defaultMessage: + "Customize the prompts that define goose's behavior in different contexts. These prompts use Jinja2 templating syntax. Be careful when modifying template variables, as incorrect changes can break functionality. Please share any improvements with the community.", }, resetAll: { id: 'promptsSettings.resetAll', @@ -122,18 +126,16 @@ const i18n = defineMessages({ export default function PromptsSettingsSection() { const intl = useIntl(); - const [prompts, setPrompts] = useState([]); + const [prompts, setPrompts] = useState([]); const [selectedPrompt, setSelectedPrompt] = useState(null); - const [promptData, setPromptData] = useState(null); + const [promptData, setPromptData] = useState(null); const [content, setContent] = useState(''); const [hasChanges, setHasChanges] = useState(false); const fetchPrompts = useCallback(async () => { try { - const response = await getPrompts(); - if (response.data) { - setPrompts(response.data.prompts); - } + const prompts = await acpListPrompts(); + setPrompts(prompts); } catch (error) { console.error('Failed to fetch prompts:', error); toast.error(intl.formatMessage(i18n.failedToLoadPrompts)); @@ -148,11 +150,9 @@ export default function PromptsSettingsSection() { if (selectedPrompt) { const fetchPrompt = async () => { try { - const response = await getPrompt({ path: { name: selectedPrompt } }); - if (response.data) { - setPromptData(response.data); - setContent(response.data.content); - } + const prompt = await acpGetPrompt(selectedPrompt); + setPromptData(prompt); + setContent(prompt.content); } catch (error) { console.error('Failed to fetch prompt:', error); toast.error(intl.formatMessage(i18n.failedToLoadPrompt)); @@ -169,18 +169,14 @@ export default function PromptsSettingsSection() { }, [content, promptData]); const handleResetAll = async () => { - if ( - !window.confirm( - intl.formatMessage(i18n.confirmResetAll) - ) - ) { + if (!window.confirm(intl.formatMessage(i18n.confirmResetAll))) { return; } try { - const customizedPrompts = prompts.filter((p) => p.is_customized); + const customizedPrompts = prompts.filter((p) => p.isCustomized); for (const prompt of customizedPrompts) { - await resetPrompt({ path: { name: prompt.name } }); + await acpResetPrompt(prompt.name); } toast.success(intl.formatMessage(i18n.allPromptsReset)); fetchPrompts(); @@ -193,12 +189,9 @@ export default function PromptsSettingsSection() { const handleSave = async () => { if (!selectedPrompt) return; try { - await savePrompt({ - path: { name: selectedPrompt }, - body: { content }, - }); + await acpSavePrompt(selectedPrompt, content); toast.success(intl.formatMessage(i18n.promptSaved)); - setPromptData((prev) => (prev ? { ...prev, content, is_customized: true } : null)); + setPromptData((prev) => (prev ? { ...prev, content, isCustomized: true } : null)); fetchPrompts(); } catch (error) { console.error('Failed to save prompt:', error); @@ -208,19 +201,15 @@ export default function PromptsSettingsSection() { const handleReset = async () => { if (!selectedPrompt) return; - if ( - !window.confirm( - intl.formatMessage(i18n.confirmResetOne) - ) - ) { + if (!window.confirm(intl.formatMessage(i18n.confirmResetOne))) { return; } try { - await resetPrompt({ path: { name: selectedPrompt } }); + await acpResetPrompt(selectedPrompt); if (promptData) { - setContent(promptData.default_content); - setPromptData({ ...promptData, content: promptData.default_content, is_customized: false }); + setContent(promptData.defaultContent); + setPromptData({ ...promptData, content: promptData.defaultContent, isCustomized: false }); } fetchPrompts(); toast.success(intl.formatMessage(i18n.promptResetToDefault)); @@ -237,7 +226,7 @@ export default function PromptsSettingsSection() { return; } } - setContent(promptData.default_content); + setContent(promptData.defaultContent); } }; @@ -252,7 +241,7 @@ export default function PromptsSettingsSection() { setContent(''); }; - const hasCustomizedPrompts = prompts.some((p) => p.is_customized); + const hasCustomizedPrompts = prompts.some((p) => p.isCustomized); if (selectedPrompt) { return ( @@ -270,7 +259,7 @@ export default function PromptsSettingsSection() { {intl.formatMessage(i18n.backToList)}

- {promptData?.is_customized && ( + {promptData?.isCustomized && (
- {intl.formatMessage(i18n.editPromptTitle, { name: selectedPrompt })} - {promptData?.is_customized && ( + + {intl.formatMessage(i18n.editPromptTitle, { name: selectedPrompt })} + + {promptData?.isCustomized && ( {intl.formatMessage(i18n.customized)} @@ -307,8 +298,10 @@ export default function PromptsSettingsSection() {
- - {promptData?.is_customized && content !== promptData.default_content && ( + + {promptData?.isCustomized && content !== promptData.defaultContent && ( )} - {secret.can_delete && ( + {secret.canDelete && (
-
{intl.formatMessage(i18n.currentModel)}
+
+ {intl.formatMessage(i18n.currentModel)} +

{menuModelLabel} {!isModelLoading && displayProvider && ` — ${displayProvider}`}

{shouldShowResolvedModel && resolvedDisplayModelName && (
-
{intl.formatMessage(i18n.resolvedModel)}
+
+ {intl.formatMessage(i18n.resolvedModel)} +

{resolvedDisplayModelName}

@@ -204,7 +208,9 @@ export default function ModelsBottomBar({

- {intl.formatMessage(i18n.localModelSettingsTitle, { modelName: getModelDisplayName(currentModel) })} + {intl.formatMessage(i18n.localModelSettingsTitle, { + modelName: getModelDisplayName(currentModel), + })}

- +
)} diff --git a/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/DefaultProviderSetupForm.tsx b/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/DefaultProviderSetupForm.tsx index 9d8a4ab82734..9ebb333f3ebf 100644 --- a/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/DefaultProviderSetupForm.tsx +++ b/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/DefaultProviderSetupForm.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useState, useCallback } from 'react'; import { Input } from '../../../../../ui/input'; -import { useConfig } from '../../../../../ConfigContext'; +import { acpReadProviderConfig } from '../../../../../../acp/providers'; import { ProviderDetails, ConfigKey } from '../../../../../../api'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../../../../../ui/collapsible'; import { configLabels, configPlaceholders } from '../../../../../../utils/configUtils'; @@ -95,19 +95,30 @@ export default function DefaultProviderSetupForm({ const intl = useIntl(); const [isLoading, setIsLoading] = useState(true); const [optionalExpanded, setOptionalExpanded] = useState(false); - const { read } = useConfig(); const loadConfigValues = useCallback(async () => { setIsLoading(true); try { const values: { [k: string]: ConfigInput } = {}; + let fields: Awaited> = []; + try { + fields = await acpReadProviderConfig(provider.name); + } catch { + // Provider may not be in the registry yet; fall back to defaults below. + } + const fieldByKey = new Map(fields.map((field) => [field.key, field])); + for (const parameter of parameters) { - const configKey = `${parameter.name}`; - const configValue = (await read(configKey, parameter.secret || false)) as ConfigValue; + const field = fieldByKey.get(parameter.name); - if (configValue !== undefined && configValue !== null) { - values[parameter.name] = { serverValue: configValue }; + if (field?.isSet && field.value != null) { + // Secrets come back masked from the server; preserve the masked shape + // so the form renders a placeholder rather than the raw value. + const serverValue: ConfigValue = parameter.secret + ? { maskedValue: field.value } + : field.value; + values[parameter.name] = { serverValue }; } else if (parameter.default !== undefined && parameter.default !== null) { values[parameter.name] = { value: parameter.default }; } @@ -120,7 +131,7 @@ export default function DefaultProviderSetupForm({ } finally { setIsLoading(false); } - }, [parameters, read, setConfigValues]); + }, [parameters, provider.name, setConfigValues]); useEffect(() => { loadConfigValues(); diff --git a/ui/desktop/src/components/settings/providers/modal/subcomponents/handlers/DefaultSubmitHandler.tsx b/ui/desktop/src/components/settings/providers/modal/subcomponents/handlers/DefaultSubmitHandler.tsx index 26a853f6031f..6cb5ec273c82 100644 --- a/ui/desktop/src/components/settings/providers/modal/subcomponents/handlers/DefaultSubmitHandler.tsx +++ b/ui/desktop/src/components/settings/providers/modal/subcomponents/handlers/DefaultSubmitHandler.tsx @@ -1,112 +1,29 @@ -import { getProviderModels, readConfig } from '../../../../../../api'; +import { acpSaveProviderConfig } from '../../../../../../acp/providers'; /** - * Standalone function to submit provider configuration - * Useful for components that don't want to use the hook + * Submit provider configuration through ACP. + * + * The ACP server validates the supplied fields, persists config/secret values, + * and triggers an inventory refresh in a single call, so no client-side + * rollback is required. */ export const providerConfigSubmitHandler = async ( - upsertFn: (key: string, value: unknown, isSecret: boolean) => Promise, provider: { name: string; metadata: { - config_keys?: Array<{ - name: string; - required?: boolean; - default?: unknown; - secret?: boolean; - }>; + config_keys?: Array<{ name: string; default?: unknown }>; }; }, configValues: Record ) => { - const parameters = provider.metadata.config_keys || []; - - // Save current NON-SECRET config values for rollback on failure - // We skip secrets because readConfig returns masked values for secrets, - // and upserting those masked values would corrupt the actual secret - const previousConfigValues: Record = {}; - const nonSecretParams = parameters.filter((param) => !param.secret); - - await Promise.all( - nonSecretParams.map(async (param) => { - try { - const currentValue = await readConfig({ - body: { key: param.name, is_secret: false }, - }); - if (currentValue.data) { - previousConfigValues[param.name] = { - value: currentValue.data, - isSecret: false, - }; - } - } catch { - // No previous value exists, that's fine - } - }) - ); - - const requiredParams = parameters.filter((param) => param.required); - if (requiredParams.length === 0 && parameters.length > 0) { - const allOptionalWithDefaults = parameters.every( - (param) => !param.required && param.default !== undefined - ); - if (allOptionalWithDefaults) { - const promises: Promise[] = []; - - for (const param of parameters) { - if (param.default !== undefined) { - const value = - configValues[param.name] !== undefined ? configValues[param.name] : param.default; - promises.push(upsertFn(param.name, value, param.secret === true)); - } - } - - await Promise.all(promises); - return; + const fields: { key: string; value: string }[] = []; + for (const { name, default: defaultValue } of provider.metadata.config_keys ?? []) { + const value = configValues[name] ?? defaultValue; + if (value === undefined || value === null || value === '') { + continue; } + fields.push({ key: name, value: String(value) }); } - const upsertPromises = parameters.map( - async (parameter: { - name: string; - required?: boolean; - default?: unknown; - secret?: boolean; - }) => { - if (!configValues[parameter.name] && !parameter.required) { - return; - } - - const value = - configValues[parameter.name] !== undefined - ? configValues[parameter.name] - : parameter.default; - - if (value === undefined || value === null) { - return; - } - - const configKey = `${parameter.name}`; - const isSecret = parameter.secret === true; - - await upsertFn(configKey, value, isSecret); - } - ); - - await Promise.all(upsertPromises); - - try { - await getProviderModels({ - path: { name: provider.name }, - throwOnError: true, - }); - } catch (error) { - const rollbackPromises: Promise[] = []; - for (const [key, { value, isSecret }] of Object.entries(previousConfigValues)) { - rollbackPromises.push(upsertFn(key, value, isSecret)); - } - await Promise.all(rollbackPromises); - - throw error; - } + await acpSaveProviderConfig(provider.name, fields); }; diff --git a/ui/desktop/src/components/settings/reset_provider/ResetProviderSection.tsx b/ui/desktop/src/components/settings/reset_provider/ResetProviderSection.tsx index 3a457e1f5991..18c7ca776d6d 100644 --- a/ui/desktop/src/components/settings/reset_provider/ResetProviderSection.tsx +++ b/ui/desktop/src/components/settings/reset_provider/ResetProviderSection.tsx @@ -1,6 +1,6 @@ import { Button } from '../../ui/button'; import { RefreshCw } from 'lucide-react'; -import { useConfig } from '../../ConfigContext'; +import { acpClearDefaults } from '../../../acp/providers'; import { View, ViewOptions } from '../../../utils/navigationUtils'; import { defineMessages, useIntl } from '../../../i18n'; @@ -21,12 +21,10 @@ interface ResetProviderSectionProps { export default function ResetProviderSection(_props: ResetProviderSectionProps) { const intl = useIntl(); - const { remove } = useConfig(); const handleResetProvider = async () => { try { - await remove('GOOSE_PROVIDER', false); - await remove('GOOSE_MODEL', false); + await acpClearDefaults(); window.location.reload(); } catch (error) { diff --git a/ui/desktop/src/i18n/messages/en.json b/ui/desktop/src/i18n/messages/en.json index 73f40da7acdf..bd66ca1b6eb8 100644 --- a/ui/desktop/src/i18n/messages/en.json +++ b/ui/desktop/src/i18n/messages/en.json @@ -2610,7 +2610,7 @@ "defaultMessage": "Failed to save prompt" }, "promptsSettings.promptEditingDescription": { - "defaultMessage": "Customize the prompts that define ApeMind Agent's behavior in different contexts. These prompts use Jinja2 templating syntax. Be careful when modifying template variables, as incorrect changes can break functionality." + "defaultMessage": "Customize the prompts that define goose's behavior in different contexts. These prompts use Jinja2 templating syntax. Be careful when modifying template variables, as incorrect changes can break functionality. Please share any improvements with the community." }, "promptsSettings.promptEditingTitle": { "defaultMessage": "Prompt Editing" @@ -4379,6 +4379,9 @@ "toolApprovalButtons.deny": { "defaultMessage": "Deny" }, + "toolApprovalButtons.staleApprovalRequest": { + "defaultMessage": "This approval request is no longer active." + }, "toolCallStatusIndicator.toolStatus": { "defaultMessage": "Tool status: {status}" }, diff --git a/ui/desktop/src/i18n/messages/es.json b/ui/desktop/src/i18n/messages/es.json index 212b6b1b43ef..5bad46d38066 100644 --- a/ui/desktop/src/i18n/messages/es.json +++ b/ui/desktop/src/i18n/messages/es.json @@ -4379,6 +4379,9 @@ "toolApprovalButtons.deny": { "defaultMessage": "Denegar" }, + "toolApprovalButtons.staleApprovalRequest": { + "defaultMessage": "This approval request is no longer active." + }, "toolCallStatusIndicator.toolStatus": { "defaultMessage": "Estado de la herramienta: {status}" }, diff --git a/ui/desktop/src/i18n/messages/hi.json b/ui/desktop/src/i18n/messages/hi.json index d29492bd7fcf..4cd6e70ee61b 100644 --- a/ui/desktop/src/i18n/messages/hi.json +++ b/ui/desktop/src/i18n/messages/hi.json @@ -4379,6 +4379,9 @@ "toolApprovalButtons.deny": { "defaultMessage": "इन्कार" }, + "toolApprovalButtons.staleApprovalRequest": { + "defaultMessage": "This approval request is no longer active." + }, "toolCallStatusIndicator.toolStatus": { "defaultMessage": "उपकरण स्थिति: {status}" }, diff --git a/ui/desktop/src/i18n/messages/ja.json b/ui/desktop/src/i18n/messages/ja.json index e180110d053d..05472fdfadb2 100644 --- a/ui/desktop/src/i18n/messages/ja.json +++ b/ui/desktop/src/i18n/messages/ja.json @@ -4379,6 +4379,9 @@ "toolApprovalButtons.deny": { "defaultMessage": "拒否" }, + "toolApprovalButtons.staleApprovalRequest": { + "defaultMessage": "This approval request is no longer active." + }, "toolCallStatusIndicator.toolStatus": { "defaultMessage": "ツールのステータス: {status}" }, diff --git a/ui/desktop/src/i18n/messages/ko.json b/ui/desktop/src/i18n/messages/ko.json index 7beb8342d057..502a5d10b3bb 100644 --- a/ui/desktop/src/i18n/messages/ko.json +++ b/ui/desktop/src/i18n/messages/ko.json @@ -4379,6 +4379,9 @@ "toolApprovalButtons.deny": { "defaultMessage": "거부" }, + "toolApprovalButtons.staleApprovalRequest": { + "defaultMessage": "This approval request is no longer active." + }, "toolCallStatusIndicator.toolStatus": { "defaultMessage": "도구 상태: {status}" }, diff --git a/ui/desktop/src/i18n/messages/ru.json b/ui/desktop/src/i18n/messages/ru.json index d194464fc6a1..5a5078c3e4fe 100644 --- a/ui/desktop/src/i18n/messages/ru.json +++ b/ui/desktop/src/i18n/messages/ru.json @@ -4379,6 +4379,9 @@ "toolApprovalButtons.deny": { "defaultMessage": "Отклонить" }, + "toolApprovalButtons.staleApprovalRequest": { + "defaultMessage": "This approval request is no longer active." + }, "toolCallStatusIndicator.toolStatus": { "defaultMessage": "Статус инструмента: {status}" }, diff --git a/ui/desktop/src/i18n/messages/tr.json b/ui/desktop/src/i18n/messages/tr.json index 9b8950390fc2..19390efd79df 100644 --- a/ui/desktop/src/i18n/messages/tr.json +++ b/ui/desktop/src/i18n/messages/tr.json @@ -4379,6 +4379,9 @@ "toolApprovalButtons.deny": { "defaultMessage": "Reddet" }, + "toolApprovalButtons.staleApprovalRequest": { + "defaultMessage": "This approval request is no longer active." + }, "toolCallStatusIndicator.toolStatus": { "defaultMessage": "Takım durumu: {status}" }, diff --git a/ui/desktop/src/i18n/messages/zh-CN.json b/ui/desktop/src/i18n/messages/zh-CN.json index 80f846c1ffd8..a2c2a7dc5442 100644 --- a/ui/desktop/src/i18n/messages/zh-CN.json +++ b/ui/desktop/src/i18n/messages/zh-CN.json @@ -4379,6 +4379,9 @@ "toolApprovalButtons.deny": { "defaultMessage": "拒绝" }, + "toolApprovalButtons.staleApprovalRequest": { + "defaultMessage": "This approval request is no longer active." + }, "toolCallStatusIndicator.toolStatus": { "defaultMessage": "工具状态:{status}" }, diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index 2b72d668d5f2..6f166939ea6a 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -806,6 +806,7 @@ let appConfig = { GOOSE_LOCALE: process.env.GOOSE_LOCALE || undefined, // If GOOSE_ALLOWLIST_WARNING env var is not set, defaults to false (strict blocking mode) GOOSE_ALLOWLIST_WARNING: process.env.GOOSE_ALLOWLIST_WARNING === 'true', + GOOSE_DISABLE_NOSTR_SHARING: process.env.GOOSE_DISABLE_NOSTR_SHARING === 'true', }; const windowMap = new Map(); diff --git a/ui/desktop/src/renderer.tsx b/ui/desktop/src/renderer.tsx index 71feae77ca01..97a5d6e85a45 100644 --- a/ui/desktop/src/renderer.tsx +++ b/ui/desktop/src/renderer.tsx @@ -6,7 +6,7 @@ import { ErrorBoundary } from './components/ErrorBoundary'; import SuspenseLoader from './suspense-loader'; import { client } from './api/client.gen'; import { setTelemetryEnabled } from './utils/analytics'; -import { readConfig } from './api'; +import { acpReadConfig } from './acp/config'; import { applyThemeTokens } from './theme/theme-tokens'; import { currentLocale, currentMessageLocale, loadMessages } from './i18n'; @@ -50,10 +50,8 @@ function handleIntlError(err: { code: string; message?: string }) { }); try { - const telemetryResponse = await readConfig({ - body: { key: TELEMETRY_CONFIG_KEY, is_secret: false }, - }); - const isTelemetryEnabled = telemetryResponse.data !== false; + const telemetryValue = await acpReadConfig(TELEMETRY_CONFIG_KEY, false); + const isTelemetryEnabled = telemetryValue !== false; setTelemetryEnabled(isTelemetryEnabled); } catch (error) { console.warn('[Analytics] Failed to initialize analytics:', error); diff --git a/ui/desktop/src/utils/autoUpdater.ts b/ui/desktop/src/utils/autoUpdater.ts index 8fcdf3cbd9b4..69c931e36dba 100644 --- a/ui/desktop/src/utils/autoUpdater.ts +++ b/ui/desktop/src/utils/autoUpdater.ts @@ -295,7 +295,7 @@ export function registerUpdateIpcHandlers() { type: 'info', title: 'Update Ready to Install', message: `Version ${githubUpdateInfo.latestVersion} is ready to install.`, - detail: `The update has been downloaded and extracted. To complete the installation:\n\n1. Click "Open Folder" to view the new Goose.app\n2. Quit Goose (this app will close)\n3. Drag the new Goose.app to your Applications folder\n4. Replace the existing app when prompted\n\nThe update will be available the next time you launch Goose.`, + detail: `The update has been downloaded and extracted. To complete the installation:\n\n1. Click "Open Folder" to view the new ApeMind Agent.app\n2. Quit ApeMind Agent (this app will close)\n3. Drag the new ApeMind Agent.app to your Applications folder\n4. Replace the existing app when prompted\n\nThe update will be available the next time you launch ApeMind Agent.`, buttons: ['Open Folder & Quit', 'Open Folder Only', 'Cancel'], defaultId: 0, cancelId: 2, @@ -675,7 +675,7 @@ export function setupAutoUpdater(tray?: Tray) { // Show native notification const notification = new Notification({ title: 'Update Ready', - body: `Version ${info.version} will be installed when you quit Goose. Click to install now.`, + body: `Version ${info.version} will be installed when you quit ApeMind Agent. Click to install now.`, }); notification.show(); @@ -766,7 +766,7 @@ function updateTrayIcon(hasUpdate: boolean) { } else { iconPath = path.join(process.resourcesPath, 'images', 'iconTemplateUpdate.png'); } - trayRef.setToolTip('Goose - Update Available'); + trayRef.setToolTip('ApeMind Agent - Update Available'); } else { // Use normal icon if (isDev) { @@ -774,7 +774,7 @@ function updateTrayIcon(hasUpdate: boolean) { } else { iconPath = path.join(process.resourcesPath, 'images', 'iconTemplate.png'); } - trayRef.setToolTip('Goose'); + trayRef.setToolTip('ApeMind Agent'); } const icon = nativeImage.createFromPath(iconPath); diff --git a/ui/desktop/src/utils/canonical.ts b/ui/desktop/src/utils/canonical.ts index 6ee98da6052b..0b0ac91e9963 100644 --- a/ui/desktop/src/utils/canonical.ts +++ b/ui/desktop/src/utils/canonical.ts @@ -2,7 +2,9 @@ * Utilities for fetching canonical model information from the backend */ -import { getCanonicalModelInfo, type ModelInfoData } from '../api'; +import { acpGetCanonicalModelInfo, type CanonicalModelInfoDto } from '../acp/providers'; + +export type CanonicalModelInfo = CanonicalModelInfoDto; /** * Fetch canonical model info (pricing + context limits) for a specific provider/model @@ -10,14 +12,9 @@ import { getCanonicalModelInfo, type ModelInfoData } from '../api'; export async function fetchCanonicalModelInfo( provider: string, model: string -): Promise { +): Promise { try { - const response = await getCanonicalModelInfo({ - body: { provider, model }, - throwOnError: true, - }); - - return response.data.model_info ?? null; + return await acpGetCanonicalModelInfo(provider, model); } catch { return null; } diff --git a/ui/desktop/vitest.config.ts b/ui/desktop/vitest.config.ts index f745b9244dcc..57c3258c2515 100644 --- a/ui/desktop/vitest.config.ts +++ b/ui/desktop/vitest.config.ts @@ -16,6 +16,7 @@ const cfg = { setupFiles: ['./src/test/setup.ts'], css: true, include: ['src/**/*.{test,spec}.{js,jsx,ts,tsx}'], + testTimeout: 10000, }, } satisfies Record; diff --git a/ui/sdk/src/generated/client.gen.ts b/ui/sdk/src/generated/client.gen.ts index 360e403ae0b4..beb4b3e460fc 100644 --- a/ui/sdk/src/generated/client.gen.ts +++ b/ui/sdk/src/generated/client.gen.ts @@ -18,6 +18,14 @@ import type { AppsListRequest_unstable, AppsListResponse_unstable, ArchiveSessionRequest_unstable, + CanonicalModelInfoRequest_unstable, + CanonicalModelInfoResponse_unstable, + ConfigReadAllRequest_unstable, + ConfigReadAllResponse_unstable, + ConfigReadRequest_unstable, + ConfigReadResponse_unstable, + ConfigRemoveRequest_unstable, + ConfigUpsertRequest_unstable, CreateScheduleRequest_unstable, CreateScheduleResponse_unstable, CreateSourceRequest_unstable, @@ -32,6 +40,7 @@ import type { CustomProviderUpdateResponse_unstable, DecodeRecipeRequest_unstable, DecodeRecipeResponse_unstable, + DefaultsClearRequest_unstable, DefaultsReadRequest_unstable, DefaultsReadResponse_unstable, DefaultsSaveRequest_unstable, @@ -65,6 +74,8 @@ import type { GetAvailableExtensionsResponse_unstable, GetConfigExtensionsRequest_unstable, GetConfigExtensionsResponse_unstable, + GetPromptRequest_unstable, + GetPromptResponse_unstable, GetSessionExtensionsRequest_unstable, GetSessionExtensionsResponse_unstable, GetSessionInfoRequest_unstable, @@ -84,6 +95,8 @@ import type { KillRunningJobResponse_unstable, ListAgentMentionsRequest_unstable, ListAgentMentionsResponse_unstable, + ListPromptsRequest_unstable, + ListPromptsResponse_unstable, ListProvidersRequest_unstable, ListProvidersResponse_unstable, ListRecipesRequest_unstable, @@ -107,6 +120,7 @@ import type { PreferencesReadResponse_unstable, PreferencesRemoveRequest_unstable, PreferencesSaveRequest_unstable, + PromptOperationResponse_unstable, ProviderCatalogListRequest_unstable, ProviderCatalogListResponse_unstable, ProviderCatalogTemplateRequest_unstable, @@ -119,6 +133,9 @@ import type { ProviderConfigSaveRequest_unstable, ProviderConfigStatusRequest_unstable, ProviderConfigStatusResponse_unstable, + ProviderSecretDeleteRequest_unstable, + ProviderSecretsListRequest_unstable, + ProviderSecretsListResponse_unstable, ProviderSetupCatalogListRequest_unstable, ProviderSetupCatalogListResponse_unstable, ProviderSupportedModelsListRequest_unstable, @@ -134,8 +151,10 @@ import type { RemoveSessionExtensionRequest_unstable, RenameSessionRequest_unstable, RequestRecipeParams_unstable, + ResetPromptRequest_unstable, RunScheduleNowRequest_unstable, RunScheduleNowResponse_unstable, + SavePromptRequest_unstable, SaveRecipeRequest_unstable, SaveRecipeResponse_unstable, ScanRecipeRequest_unstable, @@ -144,6 +163,8 @@ import type { SetConfigExtensionEnabledRequest_unstable, SetRecipeSlashCommandRequest_unstable, SetSessionSystemPromptRequest_unstable, + SetToolPermissionsRequest_unstable, + SetToolPermissionsResponse_unstable, ShareSessionNostrRequest_unstable, ShareSessionNostrResponse_unstable, SteerSessionRequest_unstable, @@ -162,6 +183,9 @@ import { zAppsExportResponse_unstable, zAppsImportResponse_unstable, zAppsListResponse_unstable, + zCanonicalModelInfoResponse_unstable, + zConfigReadAllResponse_unstable, + zConfigReadResponse_unstable, zCreateScheduleResponse_unstable, zCreateSourceResponse_unstable, zCustomProviderCreateResponse_unstable, @@ -180,6 +204,7 @@ import { zExportSourceResponse_unstable, zGetAvailableExtensionsResponse_unstable, zGetConfigExtensionsResponse_unstable, + zGetPromptResponse_unstable, zGetSessionExtensionsResponse_unstable, zGetSessionInfoResponse_unstable, zGetToolsResponse_unstable, @@ -190,6 +215,7 @@ import { zInspectRunningJobResponse_unstable, zKillRunningJobResponse_unstable, zListAgentMentionsResponse_unstable, + zListPromptsResponse_unstable, zListProvidersResponse_unstable, zListRecipesResponse_unstable, zListScheduleSessionsResponse_unstable, @@ -200,11 +226,13 @@ import { zOnboardingImportScanResponse_unstable, zParseRecipeResponse_unstable, zPreferencesReadResponse_unstable, + zPromptOperationResponse_unstable, zProviderCatalogListResponse_unstable, zProviderCatalogTemplateResponse_unstable, zProviderConfigChangeResponse_unstable, zProviderConfigReadResponse_unstable, zProviderConfigStatusResponse_unstable, + zProviderSecretsListResponse_unstable, zProviderSetupCatalogListResponse_unstable, zProviderSupportedModelsListResponse_unstable, zReadResourceResponse_unstable, @@ -214,6 +242,7 @@ import { zRunScheduleNowResponse_unstable, zSaveRecipeResponse_unstable, zScanRecipeResponse_unstable, + zSetToolPermissionsResponse_unstable, zShareSessionNostrResponse_unstable, zSteerSessionResponse_unstable, zUpdateScheduleResponse_unstable, @@ -245,6 +274,18 @@ export class GooseExtClient { return zGetToolsResponse_unstable.parse(raw) as GetToolsResponse_unstable; } + async toolsPermissionsSet_unstable( + params: SetToolPermissionsRequest_unstable, + ): Promise { + const raw = await this.conn.extMethod( + "_goose/unstable/tools/permissions/set", + params, + ); + return zSetToolPermissionsResponse_unstable.parse( + raw, + ) as SetToolPermissionsResponse_unstable; + } + async toolsCall_unstable( params: GooseToolCallRequest_unstable, ): Promise { @@ -339,6 +380,52 @@ export class GooseExtClient { ) as DiagnosticsGetResponse_unstable; } + async configPromptsList_unstable( + params: ListPromptsRequest_unstable, + ): Promise { + const raw = await this.conn.extMethod( + "_goose/unstable/config/prompts/list", + params, + ); + return zListPromptsResponse_unstable.parse( + raw, + ) as ListPromptsResponse_unstable; + } + + async configPromptsGet_unstable( + params: GetPromptRequest_unstable, + ): Promise { + const raw = await this.conn.extMethod( + "_goose/unstable/config/prompts/get", + params, + ); + return zGetPromptResponse_unstable.parse(raw) as GetPromptResponse_unstable; + } + + async configPromptsSave_unstable( + params: SavePromptRequest_unstable, + ): Promise { + const raw = await this.conn.extMethod( + "_goose/unstable/config/prompts/save", + params, + ); + return zPromptOperationResponse_unstable.parse( + raw, + ) as PromptOperationResponse_unstable; + } + + async configPromptsReset_unstable( + params: ResetPromptRequest_unstable, + ): Promise { + const raw = await this.conn.extMethod( + "_goose/unstable/config/prompts/reset", + params, + ); + return zPromptOperationResponse_unstable.parse( + raw, + ) as PromptOperationResponse_unstable; + } + async sessionDelete(params: DeleteSessionRequest): Promise { await this.conn.extMethod("session/delete", params); } @@ -583,6 +670,39 @@ export class GooseExtClient { ) as ProviderConfigChangeResponse_unstable; } + async providersSecretsList_unstable( + params: ProviderSecretsListRequest_unstable, + ): Promise { + const raw = await this.conn.extMethod( + "_goose/unstable/providers/secrets/list", + params, + ); + return zProviderSecretsListResponse_unstable.parse( + raw, + ) as ProviderSecretsListResponse_unstable; + } + + async providersSecretsDelete_unstable( + params: ProviderSecretDeleteRequest_unstable, + ): Promise { + await this.conn.extMethod( + "_goose/unstable/providers/secrets/delete", + params, + ); + } + + async providersCanonicalModelInfo_unstable( + params: CanonicalModelInfoRequest_unstable, + ): Promise { + const raw = await this.conn.extMethod( + "_goose/unstable/providers/canonical-model-info", + params, + ); + return zCanonicalModelInfoResponse_unstable.parse( + raw, + ) as CanonicalModelInfoResponse_unstable; + } + async preferencesRead_unstable( params: PreferencesReadRequest_unstable, ): Promise { @@ -607,6 +727,42 @@ export class GooseExtClient { await this.conn.extMethod("_goose/unstable/preferences/remove", params); } + async configRead_unstable( + params: ConfigReadRequest_unstable, + ): Promise { + const raw = await this.conn.extMethod( + "_goose/unstable/config/read", + params, + ); + return zConfigReadResponse_unstable.parse( + raw, + ) as ConfigReadResponse_unstable; + } + + async configUpsert_unstable( + params: ConfigUpsertRequest_unstable, + ): Promise { + await this.conn.extMethod("_goose/unstable/config/upsert", params); + } + + async configRemove_unstable( + params: ConfigRemoveRequest_unstable, + ): Promise { + await this.conn.extMethod("_goose/unstable/config/remove", params); + } + + async configReadAll_unstable( + params: ConfigReadAllRequest_unstable, + ): Promise { + const raw = await this.conn.extMethod( + "_goose/unstable/config/read-all", + params, + ); + return zConfigReadAllResponse_unstable.parse( + raw, + ) as ConfigReadAllResponse_unstable; + } + async defaultsRead_unstable( params: DefaultsReadRequest_unstable, ): Promise { @@ -631,6 +787,18 @@ export class GooseExtClient { ) as DefaultsReadResponse_unstable; } + async defaultsClear_unstable( + params: DefaultsClearRequest_unstable, + ): Promise { + const raw = await this.conn.extMethod( + "_goose/unstable/defaults/clear", + params, + ); + return zDefaultsReadResponse_unstable.parse( + raw, + ) as DefaultsReadResponse_unstable; + } + async onboardingImportScan_unstable( params: OnboardingImportScanRequest_unstable, ): Promise { diff --git a/ui/sdk/src/generated/index.ts b/ui/sdk/src/generated/index.ts index e20c3c1dfbad..a16b38709b50 100644 --- a/ui/sdk/src/generated/index.ts +++ b/ui/sdk/src/generated/index.ts @@ -1,6 +1,6 @@ // This file is auto-generated by @hey-api/openapi-ts -export type { AddConfigExtensionRequest_unstable, AddSessionExtensionRequest_unstable, AgentMention, Annotations, AppsExportRequest_unstable, AppsExportResponse_unstable, AppsImportRequest_unstable, AppsImportResponse_unstable, AppsListRequest_unstable, AppsListResponse_unstable, ArchiveSessionRequest_unstable, AudioContent, AvailableCommand, AvailableCommandInput, BlobResourceContents, ContentBlock, CreateScheduleRequest_unstable, CreateScheduleResponse_unstable, CreateSourceRequest_unstable, CreateSourceResponse_unstable, CustomProviderConfigDto, CustomProviderCreateRequest_unstable, CustomProviderCreateResponse_unstable, CustomProviderDeleteRequest_unstable, CustomProviderDeleteResponse_unstable, CustomProviderReadRequest_unstable, CustomProviderReadResponse_unstable, CustomProviderUpdateRequest_unstable, CustomProviderUpdateResponse_unstable, DecodeRecipeRequest_unstable, DecodeRecipeResponse_unstable, DefaultsReadRequest_unstable, DefaultsReadResponse_unstable, DefaultsSaveRequest_unstable, DeleteRecipeRequest_unstable, DeleteScheduleRequest_unstable, DeleteSessionRequest, DeleteSourceRequest_unstable, DiagnosticsGetRequest_unstable, DiagnosticsGetResponse_unstable, DiagnosticsReportLevel, DictationConfigRequest_unstable, DictationConfigResponse_unstable, DictationDownloadProgress, DictationLocalModelStatus, DictationModelCancelRequest_unstable, DictationModelDeleteRequest_unstable, DictationModelDownloadProgressRequest_unstable, DictationModelDownloadProgressResponse_unstable, DictationModelDownloadRequest_unstable, DictationModelOption, DictationModelSelectRequest_unstable, DictationModelsListRequest_unstable, DictationModelsListResponse_unstable, DictationProviderStatusEntry, DictationSecretDeleteRequest_unstable, DictationSecretSaveRequest_unstable, DictationTranscribeRequest_unstable, DictationTranscribeResponse_unstable, EmbeddedResource, EmbeddedResourceResource, EmptyResponse, EncodeRecipeRequest_unstable, EncodeRecipeResponse_unstable, EnvVariable, ExportSessionRequest_unstable, ExportSessionResponse_unstable, ExportSourceRequest_unstable, ExportSourceResponse_unstable, ExtAgentRequest, ExtAgentResponse, ExtNotification, ExtRequest, ExtResponse, GetAvailableExtensionsRequest_unstable, GetAvailableExtensionsResponse_unstable, GetConfigExtensionsRequest_unstable, GetConfigExtensionsResponse_unstable, GetSessionExtensionsRequest_unstable, GetSessionExtensionsResponse_unstable, GetSessionInfoRequest_unstable, GetSessionInfoResponse_unstable, GetToolsRequest_unstable, GetToolsResponse_unstable, GooseExtension, GooseExtensionEntry, GooseSessionNotification_unstable, GooseSessionUpdate, GooseToolCallRequest_unstable, GooseToolCallResponse_unstable, HttpHeader, ImageContent, ImportSessionRequest_unstable, ImportSessionResponse_unstable, ImportSourcesRequest_unstable, ImportSourcesResponse_unstable, InspectRunningJobRequest_unstable, InspectRunningJobResponse_unstable, KillRunningJobRequest_unstable, KillRunningJobResponse_unstable, ListAgentMentionsRequest_unstable, ListAgentMentionsResponse_unstable, ListProvidersRequest_unstable, ListProvidersResponse_unstable, ListRecipesRequest_unstable, ListRecipesResponse_unstable, ListScheduleSessionsRequest_unstable, ListScheduleSessionsResponse_unstable, ListSchedulesRequest_unstable, ListSchedulesResponse_unstable, ListSlashCommandsRequest_unstable, ListSlashCommandsResponse_unstable, ListSourcesRequest_unstable, ListSourcesResponse_unstable, McpServer, McpServerHttp, McpServerSse, McpServerStdio, OnboardingImportApplyRequest_unstable, OnboardingImportApplyResponse_unstable, OnboardingImportCandidate, OnboardingImportCounts, OnboardingImportScanRequest_unstable, OnboardingImportScanResponse_unstable, OnboardingImportSourceKind, ParseRecipeRequest_unstable, ParseRecipeResponse_unstable, PauseScheduleRequest_unstable, PreferenceKey, PreferencesReadRequest_unstable, PreferencesReadResponse_unstable, PreferencesRemoveRequest_unstable, PreferencesSaveRequest_unstable, PreferenceValue, ProviderCatalogListRequest_unstable, ProviderCatalogListResponse_unstable, ProviderCatalogTemplateRequest_unstable, ProviderCatalogTemplateResponse_unstable, ProviderConfigAuthenticateRequest_unstable, ProviderConfigChangeResponse_unstable, ProviderConfigDeleteRequest_unstable, ProviderConfigFieldUpdate, ProviderConfigFieldValueDto, ProviderConfigKey, ProviderConfigReadRequest_unstable, ProviderConfigReadResponse_unstable, ProviderConfigSaveRequest_unstable, ProviderConfigStatusDto, ProviderConfigStatusRequest_unstable, ProviderConfigStatusResponse_unstable, ProviderInventoryEntryDto, ProviderInventoryModelDto, ProviderSetupCatalogEntryDto, ProviderSetupCatalogListRequest_unstable, ProviderSetupCatalogListResponse_unstable, ProviderSetupCategoryDto, ProviderSetupFieldDto, ProviderSetupGroupDto, ProviderSetupMethodDto, ProviderSupportedModelsListRequest_unstable, ProviderSupportedModelsListResponse_unstable, ProviderTemplateCapabilitiesDto, ProviderTemplateCatalogEntryDto, ProviderTemplateDto, ProviderTemplateModelDto, ReadResourceRequest_unstable, ReadResourceResponse_unstable, RecipeAuthorDto, RecipeDto, RecipeExtensionDto, RecipeListEntryDto, RecipeParameterDto, RecipeParameterInputTypeDto, RecipeParameterRequirementDto, RecipeParamsAction, RecipeParamsResponse_unstable, RecipeResponseDto, RecipeRetryConfigDto, RecipeSettingsDto, RecipeSuccessCheckDto, RecipeToYamlRequest_unstable, RecipeToYamlResponse_unstable, RefreshProviderInventoryRequest_unstable, RefreshProviderInventoryResponse_unstable, RefreshProviderInventorySkipDto, RefreshProviderInventorySkipReasonDto, RemoveConfigExtensionRequest_unstable, RemoveSessionExtensionRequest_unstable, RenameSessionRequest_unstable, RequestRecipeParams_unstable, ResourceLink, Role, RunScheduleNowRequest_unstable, RunScheduleNowResponse_unstable, RunScheduleNowStatus, SaveRecipeRequest_unstable, SaveRecipeResponse_unstable, ScanRecipeRequest_unstable, ScanRecipeResponse_unstable, ScheduledJobDto, ScheduleRecipeRequest_unstable, SessionId, SessionImportSource, SessionInfo, SessionSystemPromptMode, SessionUsageUpdate, SetConfigExtensionEnabledRequest_unstable, SetRecipeSlashCommandRequest_unstable, SetSessionSystemPromptRequest_unstable, ShareSessionNostrRequest_unstable, ShareSessionNostrResponse_unstable, SourceEntry, SourceScope, SourceType, StatusMessage, StatusMessageUpdate, SteerSessionRequest_unstable, SteerSessionResponse_unstable, SubRecipeDto, TextContent, TextResourceContents, TruncateSessionConversationRequest_unstable, UnarchiveSessionRequest_unstable, UnpauseScheduleRequest_unstable, UnstructuredCommandInput, UpdateScheduleRequest_unstable, UpdateScheduleResponse_unstable, UpdateSessionProjectRequest_unstable, UpdateSourceRequest_unstable, UpdateSourceResponse_unstable, UpdateWorkingDirRequest_unstable } from './types.gen.js'; +export type { AddConfigExtensionRequest_unstable, AddSessionExtensionRequest_unstable, AgentMention, Annotations, AppsExportRequest_unstable, AppsExportResponse_unstable, AppsImportRequest_unstable, AppsImportResponse_unstable, AppsListRequest_unstable, AppsListResponse_unstable, ArchiveSessionRequest_unstable, AudioContent, AvailableCommand, AvailableCommandInput, BlobResourceContents, CanonicalModelInfoDto, CanonicalModelInfoRequest_unstable, CanonicalModelInfoResponse_unstable, ConfigReadAllRequest_unstable, ConfigReadAllResponse_unstable, ConfigReadRequest_unstable, ConfigReadResponse_unstable, ConfigRemoveRequest_unstable, ConfigUpsertRequest_unstable, ContentBlock, CreateScheduleRequest_unstable, CreateScheduleResponse_unstable, CreateSourceRequest_unstable, CreateSourceResponse_unstable, CustomProviderConfigDto, CustomProviderCreateRequest_unstable, CustomProviderCreateResponse_unstable, CustomProviderDeleteRequest_unstable, CustomProviderDeleteResponse_unstable, CustomProviderReadRequest_unstable, CustomProviderReadResponse_unstable, CustomProviderUpdateRequest_unstable, CustomProviderUpdateResponse_unstable, DecodeRecipeRequest_unstable, DecodeRecipeResponse_unstable, DefaultsClearRequest_unstable, DefaultsReadRequest_unstable, DefaultsReadResponse_unstable, DefaultsSaveRequest_unstable, DeleteRecipeRequest_unstable, DeleteScheduleRequest_unstable, DeleteSessionRequest, DeleteSourceRequest_unstable, DiagnosticsGetRequest_unstable, DiagnosticsGetResponse_unstable, DiagnosticsReportLevel, DictationConfigRequest_unstable, DictationConfigResponse_unstable, DictationDownloadProgress, DictationLocalModelStatus, DictationModelCancelRequest_unstable, DictationModelDeleteRequest_unstable, DictationModelDownloadProgressRequest_unstable, DictationModelDownloadProgressResponse_unstable, DictationModelDownloadRequest_unstable, DictationModelOption, DictationModelSelectRequest_unstable, DictationModelsListRequest_unstable, DictationModelsListResponse_unstable, DictationProviderStatusEntry, DictationSecretDeleteRequest_unstable, DictationSecretSaveRequest_unstable, DictationTranscribeRequest_unstable, DictationTranscribeResponse_unstable, EmbeddedResource, EmbeddedResourceResource, EmptyResponse, EncodeRecipeRequest_unstable, EncodeRecipeResponse_unstable, EnvVariable, ExportSessionRequest_unstable, ExportSessionResponse_unstable, ExportSourceRequest_unstable, ExportSourceResponse_unstable, ExtAgentRequest, ExtAgentResponse, ExtNotification, ExtRequest, ExtResponse, GetAvailableExtensionsRequest_unstable, GetAvailableExtensionsResponse_unstable, GetConfigExtensionsRequest_unstable, GetConfigExtensionsResponse_unstable, GetPromptRequest_unstable, GetPromptResponse_unstable, GetSessionExtensionsRequest_unstable, GetSessionExtensionsResponse_unstable, GetSessionInfoRequest_unstable, GetSessionInfoResponse_unstable, GetToolsRequest_unstable, GetToolsResponse_unstable, GooseExtension, GooseExtensionEntry, GooseSessionNotification_unstable, GooseSessionUpdate, GooseToolCallRequest_unstable, GooseToolCallResponse_unstable, HttpHeader, ImageContent, ImportSessionRequest_unstable, ImportSessionResponse_unstable, ImportSourcesRequest_unstable, ImportSourcesResponse_unstable, InspectRunningJobRequest_unstable, InspectRunningJobResponse_unstable, KillRunningJobRequest_unstable, KillRunningJobResponse_unstable, ListAgentMentionsRequest_unstable, ListAgentMentionsResponse_unstable, ListPromptsRequest_unstable, ListPromptsResponse_unstable, ListProvidersRequest_unstable, ListProvidersResponse_unstable, ListRecipesRequest_unstable, ListRecipesResponse_unstable, ListScheduleSessionsRequest_unstable, ListScheduleSessionsResponse_unstable, ListSchedulesRequest_unstable, ListSchedulesResponse_unstable, ListSlashCommandsRequest_unstable, ListSlashCommandsResponse_unstable, ListSourcesRequest_unstable, ListSourcesResponse_unstable, McpServer, McpServerHttp, McpServerSse, McpServerStdio, OnboardingImportApplyRequest_unstable, OnboardingImportApplyResponse_unstable, OnboardingImportCandidate, OnboardingImportCounts, OnboardingImportScanRequest_unstable, OnboardingImportScanResponse_unstable, OnboardingImportSourceKind, ParseRecipeRequest_unstable, ParseRecipeResponse_unstable, PauseScheduleRequest_unstable, PreferenceKey, PreferencesReadRequest_unstable, PreferencesReadResponse_unstable, PreferencesRemoveRequest_unstable, PreferencesSaveRequest_unstable, PreferenceValue, PromptOperationResponse_unstable, PromptTemplateEntry, ProviderCatalogListRequest_unstable, ProviderCatalogListResponse_unstable, ProviderCatalogTemplateRequest_unstable, ProviderCatalogTemplateResponse_unstable, ProviderConfigAuthenticateRequest_unstable, ProviderConfigChangeResponse_unstable, ProviderConfigDeleteRequest_unstable, ProviderConfigFieldUpdate, ProviderConfigFieldValueDto, ProviderConfigKey, ProviderConfigReadRequest_unstable, ProviderConfigReadResponse_unstable, ProviderConfigSaveRequest_unstable, ProviderConfigStatusDto, ProviderConfigStatusRequest_unstable, ProviderConfigStatusResponse_unstable, ProviderInventoryEntryDto, ProviderInventoryModelDto, ProviderSecretDeleteRequest_unstable, ProviderSecretDto, ProviderSecretsListRequest_unstable, ProviderSecretsListResponse_unstable, ProviderSecretStatusDto, ProviderSecretStorageDto, ProviderSetupCatalogEntryDto, ProviderSetupCatalogListRequest_unstable, ProviderSetupCatalogListResponse_unstable, ProviderSetupCategoryDto, ProviderSetupFieldDto, ProviderSetupGroupDto, ProviderSetupMethodDto, ProviderSupportedModelsListRequest_unstable, ProviderSupportedModelsListResponse_unstable, ProviderTemplateCapabilitiesDto, ProviderTemplateCatalogEntryDto, ProviderTemplateDto, ProviderTemplateModelDto, ReadResourceRequest_unstable, ReadResourceResponse_unstable, RecipeAuthorDto, RecipeDto, RecipeExtensionDto, RecipeListEntryDto, RecipeParameterDto, RecipeParameterInputTypeDto, RecipeParameterRequirementDto, RecipeParamsAction, RecipeParamsResponse_unstable, RecipeResponseDto, RecipeRetryConfigDto, RecipeSettingsDto, RecipeSuccessCheckDto, RecipeToYamlRequest_unstable, RecipeToYamlResponse_unstable, RefreshProviderInventoryRequest_unstable, RefreshProviderInventoryResponse_unstable, RefreshProviderInventorySkipDto, RefreshProviderInventorySkipReasonDto, RemoveConfigExtensionRequest_unstable, RemoveSessionExtensionRequest_unstable, RenameSessionRequest_unstable, RequestRecipeParams_unstable, ResetPromptRequest_unstable, ResourceLink, Role, RunScheduleNowRequest_unstable, RunScheduleNowResponse_unstable, RunScheduleNowStatus, SavePromptRequest_unstable, SaveRecipeRequest_unstable, SaveRecipeResponse_unstable, ScanRecipeRequest_unstable, ScanRecipeResponse_unstable, ScheduledJobDto, ScheduleRecipeRequest_unstable, SessionId, SessionImportSource, SessionInfo, SessionSystemPromptMode, SessionUsageUpdate, SetConfigExtensionEnabledRequest_unstable, SetRecipeSlashCommandRequest_unstable, SetSessionSystemPromptRequest_unstable, SetToolPermissionsRequest_unstable, SetToolPermissionsResponse_unstable, ShareSessionNostrRequest_unstable, ShareSessionNostrResponse_unstable, SourceEntry, SourceScope, SourceType, StatusMessage, StatusMessageUpdate, SteerSessionRequest_unstable, SteerSessionResponse_unstable, SubRecipeDto, TextContent, TextResourceContents, ToolListItem, ToolPermissionEntry, ToolPermissionLevel, TruncateSessionConversationRequest_unstable, UnarchiveSessionRequest_unstable, UnpauseScheduleRequest_unstable, UnstructuredCommandInput, UpdateScheduleRequest_unstable, UpdateScheduleResponse_unstable, UpdateSessionProjectRequest_unstable, UpdateSourceRequest_unstable, UpdateSourceResponse_unstable, UpdateWorkingDirRequest_unstable } from './types.gen.js'; export const GOOSE_EXT_METHODS = [ { @@ -18,6 +18,11 @@ export const GOOSE_EXT_METHODS = [ requestType: "GetToolsRequest_unstable", responseType: "GetToolsResponse_unstable", }, + { + method: "_goose/unstable/tools/permissions/set", + requestType: "SetToolPermissionsRequest_unstable", + responseType: "SetToolPermissionsResponse_unstable", + }, { method: "_goose/unstable/tools/call", requestType: "GooseToolCallRequest_unstable", @@ -63,6 +68,26 @@ export const GOOSE_EXT_METHODS = [ requestType: "DiagnosticsGetRequest_unstable", responseType: "DiagnosticsGetResponse_unstable", }, + { + method: "_goose/unstable/config/prompts/list", + requestType: "ListPromptsRequest_unstable", + responseType: "ListPromptsResponse_unstable", + }, + { + method: "_goose/unstable/config/prompts/get", + requestType: "GetPromptRequest_unstable", + responseType: "GetPromptResponse_unstable", + }, + { + method: "_goose/unstable/config/prompts/save", + requestType: "SavePromptRequest_unstable", + responseType: "PromptOperationResponse_unstable", + }, + { + method: "_goose/unstable/config/prompts/reset", + requestType: "ResetPromptRequest_unstable", + responseType: "PromptOperationResponse_unstable", + }, { method: "session/delete", requestType: "DeleteSessionRequest", @@ -173,6 +198,21 @@ export const GOOSE_EXT_METHODS = [ requestType: "ProviderConfigAuthenticateRequest_unstable", responseType: "ProviderConfigChangeResponse_unstable", }, + { + method: "_goose/unstable/providers/secrets/list", + requestType: "ProviderSecretsListRequest_unstable", + responseType: "ProviderSecretsListResponse_unstable", + }, + { + method: "_goose/unstable/providers/secrets/delete", + requestType: "ProviderSecretDeleteRequest_unstable", + responseType: "EmptyResponse", + }, + { + method: "_goose/unstable/providers/canonical-model-info", + requestType: "CanonicalModelInfoRequest_unstable", + responseType: "CanonicalModelInfoResponse_unstable", + }, { method: "_goose/unstable/preferences/read", requestType: "PreferencesReadRequest_unstable", @@ -188,6 +228,26 @@ export const GOOSE_EXT_METHODS = [ requestType: "PreferencesRemoveRequest_unstable", responseType: "EmptyResponse", }, + { + method: "_goose/unstable/config/read", + requestType: "ConfigReadRequest_unstable", + responseType: "ConfigReadResponse_unstable", + }, + { + method: "_goose/unstable/config/upsert", + requestType: "ConfigUpsertRequest_unstable", + responseType: "EmptyResponse", + }, + { + method: "_goose/unstable/config/remove", + requestType: "ConfigRemoveRequest_unstable", + responseType: "EmptyResponse", + }, + { + method: "_goose/unstable/config/read-all", + requestType: "ConfigReadAllRequest_unstable", + responseType: "ConfigReadAllResponse_unstable", + }, { method: "_goose/unstable/defaults/read", requestType: "DefaultsReadRequest_unstable", @@ -198,6 +258,11 @@ export const GOOSE_EXT_METHODS = [ requestType: "DefaultsSaveRequest_unstable", responseType: "DefaultsReadResponse_unstable", }, + { + method: "_goose/unstable/defaults/clear", + requestType: "DefaultsClearRequest_unstable", + responseType: "DefaultsReadResponse_unstable", + }, { method: "_goose/unstable/onboarding/import/scan", requestType: "OnboardingImportScanRequest_unstable", diff --git a/ui/sdk/src/generated/types.gen.ts b/ui/sdk/src/generated/types.gen.ts index a2319d01f971..c95c07fd2887 100644 --- a/ui/sdk/src/generated/types.gen.ts +++ b/ui/sdk/src/generated/types.gen.ts @@ -200,16 +200,53 @@ export type RemoveSessionExtensionRequest_unstable = { */ export type GetToolsRequest_unstable = { sessionId: string; + /** + * Filter tools to those belonging to this extension. + */ + extensionName?: string | null; }; /** * Tools response. */ export type GetToolsResponse_unstable = { - /** - * Array of tool info objects with `name`, `description`, `parameters`, and optional `permission`. - */ - tools: Array; + tools: Array; +}; + +/** + * A single tool item returned by the tools list endpoint. + */ +export type ToolListItem = { + name: string; + description: string; + parameters: Array; + permission?: ToolPermissionLevel | null; + inputSchema: unknown; + outputSchema?: unknown; +}; + +/** + * Permission level for a tool. + */ +export type ToolPermissionLevel = 'always_allow' | 'ask_before' | 'never_allow'; + +/** + * Set permission levels for one or more tools. + */ +export type SetToolPermissionsRequest_unstable = { + toolPermissions: Array; +}; + +/** + * A single tool permission entry. + */ +export type ToolPermissionEntry = { + toolName: string; + permission: ToolPermissionLevel; +}; + +export type SetToolPermissionsResponse_unstable = { + [key: string]: unknown; }; /** @@ -525,6 +562,61 @@ export type DiagnosticsGetResponse_unstable = { report: unknown; }; +/** + * List all available Goose prompt templates. + */ +export type ListPromptsRequest_unstable = { + [key: string]: unknown; +}; + +export type ListPromptsResponse_unstable = { + prompts: Array; +}; + +/** + * Information about a prompt template, including its default content and customization status. + */ +export type PromptTemplateEntry = { + name: string; + description: string; + defaultContent: string; + userContent?: string | null; + isCustomized: boolean; +}; + +/** + * Read a Goose prompt template. + */ +export type GetPromptRequest_unstable = { + name: string; +}; + +export type GetPromptResponse_unstable = { + name: string; + content: string; + defaultContent: string; + isCustomized: boolean; +}; + +/** + * Save a custom Goose prompt template. + */ +export type SavePromptRequest_unstable = { + name: string; + content: string; +}; + +export type PromptOperationResponse_unstable = { + message: string; +}; + +/** + * Reset a Goose prompt template to its default content. + */ +export type ResetPromptRequest_unstable = { + name: string; +}; + /** * Delete a session. */ @@ -1032,6 +1124,68 @@ export type ProviderConfigAuthenticateRequest_unstable = { providerId: string; }; +/** + * List provider credentials stored locally by Goose. + */ +export type ProviderSecretsListRequest_unstable = { + [key: string]: unknown; +}; + +export type ProviderSecretsListResponse_unstable = { + secrets: Array; +}; + +export type ProviderSecretDto = { + id: string; + provider: string; + providerDisplayName: string; + name: string; + storage: ProviderSecretStorageDto; + expiresAt?: string | null; + status: ProviderSecretStatusDto; + configured: boolean; + hasSecret: boolean; + canDelete: boolean; + canConfigure: boolean; + configureProvider?: string | null; +}; + +export type ProviderSecretStorageDto = 'secret_store' | 'provider_cache'; + +export type ProviderSecretStatusDto = 'valid' | 'expired' | 'unknown'; + +/** + * Delete a locally stored provider credential by id. + */ +export type ProviderSecretDeleteRequest_unstable = { + id: string; +}; + +/** + * Look up canonical (bundled-registry) model info for a provider/model pair. + */ +export type CanonicalModelInfoRequest_unstable = { + provider: string; + model: string; +}; + +export type CanonicalModelInfoResponse_unstable = { + modelInfo?: CanonicalModelInfoDto | null; +}; + +export type CanonicalModelInfoDto = { + provider: string; + model: string; + contextLimit: number; + maxOutputTokens?: number | null; + reasoning: boolean; + inputTokenCost?: number | null; + outputTokenCost?: number | null; + cacheReadTokenCost?: number | null; + cacheWriteTokenCost?: number | null; + currency: string; +}; + /** * Read allowlisted user preferences. Empty `keys` means all supported preferences. */ @@ -1064,6 +1218,36 @@ export type PreferencesRemoveRequest_unstable = { keys?: Array; }; +export type ConfigReadRequest_unstable = { + key: string; + isSecret?: boolean; +}; + +export type ConfigReadResponse_unstable = { + value?: unknown; +}; + +export type ConfigUpsertRequest_unstable = { + key: string; + value: unknown; + isSecret?: boolean; +}; + +export type ConfigRemoveRequest_unstable = { + key: string; + isSecret?: boolean; +}; + +export type ConfigReadAllRequest_unstable = { + [key: string]: unknown; +}; + +export type ConfigReadAllResponse_unstable = { + config: { + [key: string]: unknown; + }; +}; + /** * Read Goose default provider and model configuration. */ @@ -1084,6 +1268,13 @@ export type DefaultsSaveRequest_unstable = { modelId?: string | null; }; +/** + * Clear Goose default provider and model configuration. + */ +export type DefaultsClearRequest_unstable = { + [key: string]: unknown; +}; + /** * Scan for existing Goose and compatible app data that onboarding can import. */ @@ -2034,14 +2225,14 @@ export type RecipeParamsAction = 'submit' | 'cancel'; export type ExtRequest = { id: string; method: string; - params?: AddSessionExtensionRequest_unstable | RemoveSessionExtensionRequest_unstable | GetToolsRequest_unstable | GooseToolCallRequest_unstable | ReadResourceRequest_unstable | AppsListRequest_unstable | AppsExportRequest_unstable | AppsImportRequest_unstable | UpdateWorkingDirRequest_unstable | SetSessionSystemPromptRequest_unstable | SteerSessionRequest_unstable | DiagnosticsGetRequest_unstable | DeleteSessionRequest | GetConfigExtensionsRequest_unstable | GetAvailableExtensionsRequest_unstable | AddConfigExtensionRequest_unstable | RemoveConfigExtensionRequest_unstable | SetConfigExtensionEnabledRequest_unstable | GetSessionExtensionsRequest_unstable | ListProvidersRequest_unstable | ProviderSupportedModelsListRequest_unstable | ProviderCatalogListRequest_unstable | ProviderSetupCatalogListRequest_unstable | ProviderCatalogTemplateRequest_unstable | CustomProviderCreateRequest_unstable | CustomProviderReadRequest_unstable | CustomProviderUpdateRequest_unstable | CustomProviderDeleteRequest_unstable | RefreshProviderInventoryRequest_unstable | ProviderConfigReadRequest_unstable | ProviderConfigStatusRequest_unstable | ProviderConfigSaveRequest_unstable | ProviderConfigDeleteRequest_unstable | ProviderConfigAuthenticateRequest_unstable | PreferencesReadRequest_unstable | PreferencesSaveRequest_unstable | PreferencesRemoveRequest_unstable | DefaultsReadRequest_unstable | DefaultsSaveRequest_unstable | OnboardingImportScanRequest_unstable | OnboardingImportApplyRequest_unstable | ExportSessionRequest_unstable | ImportSessionRequest_unstable | ShareSessionNostrRequest_unstable | EncodeRecipeRequest_unstable | DecodeRecipeRequest_unstable | ScanRecipeRequest_unstable | ListRecipesRequest_unstable | DeleteRecipeRequest_unstable | ScheduleRecipeRequest_unstable | SetRecipeSlashCommandRequest_unstable | SaveRecipeRequest_unstable | ParseRecipeRequest_unstable | RecipeToYamlRequest_unstable | ListSchedulesRequest_unstable | ListScheduleSessionsRequest_unstable | CreateScheduleRequest_unstable | DeleteScheduleRequest_unstable | PauseScheduleRequest_unstable | UnpauseScheduleRequest_unstable | UpdateScheduleRequest_unstable | RunScheduleNowRequest_unstable | KillRunningJobRequest_unstable | InspectRunningJobRequest_unstable | GetSessionInfoRequest_unstable | TruncateSessionConversationRequest_unstable | UpdateSessionProjectRequest_unstable | RenameSessionRequest_unstable | ArchiveSessionRequest_unstable | UnarchiveSessionRequest_unstable | CreateSourceRequest_unstable | ListSourcesRequest_unstable | ListAgentMentionsRequest_unstable | ListSlashCommandsRequest_unstable | UpdateSourceRequest_unstable | DeleteSourceRequest_unstable | ExportSourceRequest_unstable | ImportSourcesRequest_unstable | DictationTranscribeRequest_unstable | DictationConfigRequest_unstable | DictationSecretSaveRequest_unstable | DictationSecretDeleteRequest_unstable | DictationModelsListRequest_unstable | DictationModelDownloadRequest_unstable | DictationModelDownloadProgressRequest_unstable | DictationModelCancelRequest_unstable | DictationModelDeleteRequest_unstable | DictationModelSelectRequest_unstable | { + params?: AddSessionExtensionRequest_unstable | RemoveSessionExtensionRequest_unstable | GetToolsRequest_unstable | SetToolPermissionsRequest_unstable | GooseToolCallRequest_unstable | ReadResourceRequest_unstable | AppsListRequest_unstable | AppsExportRequest_unstable | AppsImportRequest_unstable | UpdateWorkingDirRequest_unstable | SetSessionSystemPromptRequest_unstable | SteerSessionRequest_unstable | DiagnosticsGetRequest_unstable | ListPromptsRequest_unstable | GetPromptRequest_unstable | SavePromptRequest_unstable | ResetPromptRequest_unstable | DeleteSessionRequest | GetConfigExtensionsRequest_unstable | GetAvailableExtensionsRequest_unstable | AddConfigExtensionRequest_unstable | RemoveConfigExtensionRequest_unstable | SetConfigExtensionEnabledRequest_unstable | GetSessionExtensionsRequest_unstable | ListProvidersRequest_unstable | ProviderSupportedModelsListRequest_unstable | ProviderCatalogListRequest_unstable | ProviderSetupCatalogListRequest_unstable | ProviderCatalogTemplateRequest_unstable | CustomProviderCreateRequest_unstable | CustomProviderReadRequest_unstable | CustomProviderUpdateRequest_unstable | CustomProviderDeleteRequest_unstable | RefreshProviderInventoryRequest_unstable | ProviderConfigReadRequest_unstable | ProviderConfigStatusRequest_unstable | ProviderConfigSaveRequest_unstable | ProviderConfigDeleteRequest_unstable | ProviderConfigAuthenticateRequest_unstable | ProviderSecretsListRequest_unstable | ProviderSecretDeleteRequest_unstable | CanonicalModelInfoRequest_unstable | PreferencesReadRequest_unstable | PreferencesSaveRequest_unstable | PreferencesRemoveRequest_unstable | ConfigReadRequest_unstable | ConfigUpsertRequest_unstable | ConfigRemoveRequest_unstable | ConfigReadAllRequest_unstable | DefaultsReadRequest_unstable | DefaultsSaveRequest_unstable | DefaultsClearRequest_unstable | OnboardingImportScanRequest_unstable | OnboardingImportApplyRequest_unstable | ExportSessionRequest_unstable | ImportSessionRequest_unstable | ShareSessionNostrRequest_unstable | EncodeRecipeRequest_unstable | DecodeRecipeRequest_unstable | ScanRecipeRequest_unstable | ListRecipesRequest_unstable | DeleteRecipeRequest_unstable | ScheduleRecipeRequest_unstable | SetRecipeSlashCommandRequest_unstable | SaveRecipeRequest_unstable | ParseRecipeRequest_unstable | RecipeToYamlRequest_unstable | ListSchedulesRequest_unstable | ListScheduleSessionsRequest_unstable | CreateScheduleRequest_unstable | DeleteScheduleRequest_unstable | PauseScheduleRequest_unstable | UnpauseScheduleRequest_unstable | UpdateScheduleRequest_unstable | RunScheduleNowRequest_unstable | KillRunningJobRequest_unstable | InspectRunningJobRequest_unstable | GetSessionInfoRequest_unstable | TruncateSessionConversationRequest_unstable | UpdateSessionProjectRequest_unstable | RenameSessionRequest_unstable | ArchiveSessionRequest_unstable | UnarchiveSessionRequest_unstable | CreateSourceRequest_unstable | ListSourcesRequest_unstable | ListAgentMentionsRequest_unstable | ListSlashCommandsRequest_unstable | UpdateSourceRequest_unstable | DeleteSourceRequest_unstable | ExportSourceRequest_unstable | ImportSourcesRequest_unstable | DictationTranscribeRequest_unstable | DictationConfigRequest_unstable | DictationSecretSaveRequest_unstable | DictationSecretDeleteRequest_unstable | DictationModelsListRequest_unstable | DictationModelDownloadRequest_unstable | DictationModelDownloadProgressRequest_unstable | DictationModelCancelRequest_unstable | DictationModelDeleteRequest_unstable | DictationModelSelectRequest_unstable | { [key: string]: unknown; } | null; }; export type ExtResponse = { id: string; - result?: EmptyResponse | GetToolsResponse_unstable | GooseToolCallResponse_unstable | ReadResourceResponse_unstable | AppsListResponse_unstable | AppsExportResponse_unstable | AppsImportResponse_unstable | SteerSessionResponse_unstable | DiagnosticsGetResponse_unstable | GetConfigExtensionsResponse_unstable | GetAvailableExtensionsResponse_unstable | GetSessionExtensionsResponse_unstable | ListProvidersResponse_unstable | ProviderSupportedModelsListResponse_unstable | ProviderCatalogListResponse_unstable | ProviderSetupCatalogListResponse_unstable | ProviderCatalogTemplateResponse_unstable | CustomProviderCreateResponse_unstable | CustomProviderReadResponse_unstable | CustomProviderUpdateResponse_unstable | CustomProviderDeleteResponse_unstable | RefreshProviderInventoryResponse_unstable | ProviderConfigReadResponse_unstable | ProviderConfigStatusResponse_unstable | ProviderConfigChangeResponse_unstable | PreferencesReadResponse_unstable | DefaultsReadResponse_unstable | OnboardingImportScanResponse_unstable | OnboardingImportApplyResponse_unstable | ExportSessionResponse_unstable | ImportSessionResponse_unstable | ShareSessionNostrResponse_unstable | EncodeRecipeResponse_unstable | DecodeRecipeResponse_unstable | ScanRecipeResponse_unstable | ListRecipesResponse_unstable | SaveRecipeResponse_unstable | ParseRecipeResponse_unstable | RecipeToYamlResponse_unstable | ListSchedulesResponse_unstable | ListScheduleSessionsResponse_unstable | CreateScheduleResponse_unstable | UpdateScheduleResponse_unstable | RunScheduleNowResponse_unstable | KillRunningJobResponse_unstable | InspectRunningJobResponse_unstable | GetSessionInfoResponse_unstable | CreateSourceResponse_unstable | ListSourcesResponse_unstable | ListAgentMentionsResponse_unstable | ListSlashCommandsResponse_unstable | UpdateSourceResponse_unstable | ExportSourceResponse_unstable | ImportSourcesResponse_unstable | DictationTranscribeResponse_unstable | DictationConfigResponse_unstable | DictationModelsListResponse_unstable | DictationModelDownloadProgressResponse_unstable | unknown; + result?: EmptyResponse | GetToolsResponse_unstable | SetToolPermissionsResponse_unstable | GooseToolCallResponse_unstable | ReadResourceResponse_unstable | AppsListResponse_unstable | AppsExportResponse_unstable | AppsImportResponse_unstable | SteerSessionResponse_unstable | DiagnosticsGetResponse_unstable | ListPromptsResponse_unstable | GetPromptResponse_unstable | PromptOperationResponse_unstable | GetConfigExtensionsResponse_unstable | GetAvailableExtensionsResponse_unstable | GetSessionExtensionsResponse_unstable | ListProvidersResponse_unstable | ProviderSupportedModelsListResponse_unstable | ProviderCatalogListResponse_unstable | ProviderSetupCatalogListResponse_unstable | ProviderCatalogTemplateResponse_unstable | CustomProviderCreateResponse_unstable | CustomProviderReadResponse_unstable | CustomProviderUpdateResponse_unstable | CustomProviderDeleteResponse_unstable | RefreshProviderInventoryResponse_unstable | ProviderConfigReadResponse_unstable | ProviderConfigStatusResponse_unstable | ProviderConfigChangeResponse_unstable | ProviderSecretsListResponse_unstable | CanonicalModelInfoResponse_unstable | PreferencesReadResponse_unstable | ConfigReadResponse_unstable | ConfigReadAllResponse_unstable | DefaultsReadResponse_unstable | OnboardingImportScanResponse_unstable | OnboardingImportApplyResponse_unstable | ExportSessionResponse_unstable | ImportSessionResponse_unstable | ShareSessionNostrResponse_unstable | EncodeRecipeResponse_unstable | DecodeRecipeResponse_unstable | ScanRecipeResponse_unstable | ListRecipesResponse_unstable | SaveRecipeResponse_unstable | ParseRecipeResponse_unstable | RecipeToYamlResponse_unstable | ListSchedulesResponse_unstable | ListScheduleSessionsResponse_unstable | CreateScheduleResponse_unstable | UpdateScheduleResponse_unstable | RunScheduleNowResponse_unstable | KillRunningJobResponse_unstable | InspectRunningJobResponse_unstable | GetSessionInfoResponse_unstable | CreateSourceResponse_unstable | ListSourcesResponse_unstable | ListAgentMentionsResponse_unstable | ListSlashCommandsResponse_unstable | UpdateSourceResponse_unstable | ExportSourceResponse_unstable | ImportSourcesResponse_unstable | DictationTranscribeResponse_unstable | DictationConfigResponse_unstable | DictationModelsListResponse_unstable | DictationModelDownloadProgressResponse_unstable | unknown; } | { error: { code: number; diff --git a/ui/sdk/src/generated/zod.gen.ts b/ui/sdk/src/generated/zod.gen.ts index c423d140c2e2..3ce88c961970 100644 --- a/ui/sdk/src/generated/zod.gen.ts +++ b/ui/sdk/src/generated/zod.gen.ts @@ -167,16 +167,61 @@ export const zRemoveSessionExtensionRequest_unstable = z.object({ * List all tools available in a session. */ export const zGetToolsRequest_unstable = z.object({ - sessionId: z.string() + sessionId: z.string(), + extensionName: z.union([ + z.string(), + z.null() + ]).optional() +}); + +/** + * Permission level for a tool. + */ +export const zToolPermissionLevel = z.enum([ + 'always_allow', + 'ask_before', + 'never_allow' +]); + +/** + * A single tool item returned by the tools list endpoint. + */ +export const zToolListItem = z.object({ + name: z.string(), + description: z.string(), + parameters: z.array(z.string()), + permission: z.union([ + zToolPermissionLevel, + z.null() + ]).optional(), + inputSchema: z.unknown(), + outputSchema: z.unknown().optional() }); /** * Tools response. */ export const zGetToolsResponse_unstable = z.object({ - tools: z.array(z.unknown()) + tools: z.array(zToolListItem) }); +/** + * A single tool permission entry. + */ +export const zToolPermissionEntry = z.object({ + toolName: z.string(), + permission: zToolPermissionLevel +}); + +/** + * Set permission levels for one or more tools. + */ +export const zSetToolPermissionsRequest_unstable = z.object({ + toolPermissions: z.array(zToolPermissionEntry) +}); + +export const zSetToolPermissionsResponse_unstable = z.record(z.unknown()); + /** * Call a tool from an extension. */ @@ -497,6 +542,62 @@ export const zDiagnosticsGetResponse_unstable = z.object({ report: z.unknown() }); +/** + * List all available Goose prompt templates. + */ +export const zListPromptsRequest_unstable = z.record(z.unknown()); + +/** + * Information about a prompt template, including its default content and customization status. + */ +export const zPromptTemplateEntry = z.object({ + name: z.string(), + description: z.string(), + defaultContent: z.string(), + userContent: z.union([ + z.string(), + z.null() + ]).optional(), + isCustomized: z.boolean() +}); + +export const zListPromptsResponse_unstable = z.object({ + prompts: z.array(zPromptTemplateEntry) +}); + +/** + * Read a Goose prompt template. + */ +export const zGetPromptRequest_unstable = z.object({ + name: z.string() +}); + +export const zGetPromptResponse_unstable = z.object({ + name: z.string(), + content: z.string(), + defaultContent: z.string(), + isCustomized: z.boolean() +}); + +/** + * Save a custom Goose prompt template. + */ +export const zSavePromptRequest_unstable = z.object({ + name: z.string(), + content: z.string() +}); + +export const zPromptOperationResponse_unstable = z.object({ + message: z.string() +}); + +/** + * Reset a Goose prompt template to its default content. + */ +export const zResetPromptRequest_unstable = z.object({ + name: z.string() +}); + /** * Delete a session. */ @@ -1016,6 +1117,94 @@ export const zProviderConfigAuthenticateRequest_unstable = z.object({ providerId: z.string() }); +/** + * List provider credentials stored locally by Goose. + */ +export const zProviderSecretsListRequest_unstable = z.record(z.unknown()); + +export const zProviderSecretStorageDto = z.enum(['secret_store', 'provider_cache']); + +export const zProviderSecretStatusDto = z.enum([ + 'valid', + 'expired', + 'unknown' +]); + +export const zProviderSecretDto = z.object({ + id: z.string(), + provider: z.string(), + providerDisplayName: z.string(), + name: z.string(), + storage: zProviderSecretStorageDto, + expiresAt: z.union([ + z.string(), + z.null() + ]).optional(), + status: zProviderSecretStatusDto, + configured: z.boolean(), + hasSecret: z.boolean(), + canDelete: z.boolean(), + canConfigure: z.boolean(), + configureProvider: z.union([ + z.string(), + z.null() + ]).optional() +}); + +export const zProviderSecretsListResponse_unstable = z.object({ + secrets: z.array(zProviderSecretDto) +}); + +/** + * Delete a locally stored provider credential by id. + */ +export const zProviderSecretDeleteRequest_unstable = z.object({ + id: z.string() +}); + +/** + * Look up canonical (bundled-registry) model info for a provider/model pair. + */ +export const zCanonicalModelInfoRequest_unstable = z.object({ + provider: z.string(), + model: z.string() +}); + +export const zCanonicalModelInfoDto = z.object({ + provider: z.string(), + model: z.string(), + contextLimit: z.number().int().gte(0), + maxOutputTokens: z.union([ + z.number().int().gte(0), + z.null() + ]).optional(), + reasoning: z.boolean(), + inputTokenCost: z.union([ + z.number(), + z.null() + ]).optional(), + outputTokenCost: z.union([ + z.number(), + z.null() + ]).optional(), + cacheReadTokenCost: z.union([ + z.number(), + z.null() + ]).optional(), + cacheWriteTokenCost: z.union([ + z.number(), + z.null() + ]).optional(), + currency: z.string() +}); + +export const zCanonicalModelInfoResponse_unstable = z.object({ + modelInfo: z.union([ + zCanonicalModelInfoDto, + z.null() + ]).optional() +}); + export const zPreferenceKey = z.enum([ 'autoCompactThreshold', 'gooseThinkingEffort', @@ -1054,6 +1243,32 @@ export const zPreferencesRemoveRequest_unstable = z.object({ keys: z.array(zPreferenceKey).optional().default([]) }); +export const zConfigReadRequest_unstable = z.object({ + key: z.string(), + isSecret: z.boolean().optional().default(false) +}); + +export const zConfigReadResponse_unstable = z.object({ + value: z.unknown().optional().default(null) +}); + +export const zConfigUpsertRequest_unstable = z.object({ + key: z.string(), + value: z.unknown(), + isSecret: z.boolean().optional().default(false) +}); + +export const zConfigRemoveRequest_unstable = z.object({ + key: z.string(), + isSecret: z.boolean().optional().default(false) +}); + +export const zConfigReadAllRequest_unstable = z.record(z.unknown()); + +export const zConfigReadAllResponse_unstable = z.object({ + config: z.record(z.unknown()) +}); + /** * Read Goose default provider and model configuration. */ @@ -1081,6 +1296,11 @@ export const zDefaultsSaveRequest_unstable = z.object({ ]).optional() }); +/** + * Clear Goose default provider and model configuration. + */ +export const zDefaultsClearRequest_unstable = z.record(z.unknown()); + /** * Sources that onboarding knows how to discover and import. */ @@ -2164,6 +2384,7 @@ export const zExtRequest = z.object({ zAddSessionExtensionRequest_unstable, zRemoveSessionExtensionRequest_unstable, zGetToolsRequest_unstable, + zSetToolPermissionsRequest_unstable, zGooseToolCallRequest_unstable, zReadResourceRequest_unstable, zAppsListRequest_unstable, @@ -2173,6 +2394,10 @@ export const zExtRequest = z.object({ zSetSessionSystemPromptRequest_unstable, zSteerSessionRequest_unstable, zDiagnosticsGetRequest_unstable, + zListPromptsRequest_unstable, + zGetPromptRequest_unstable, + zSavePromptRequest_unstable, + zResetPromptRequest_unstable, zDeleteSessionRequest, zGetConfigExtensionsRequest_unstable, zGetAvailableExtensionsRequest_unstable, @@ -2195,11 +2420,19 @@ export const zExtRequest = z.object({ zProviderConfigSaveRequest_unstable, zProviderConfigDeleteRequest_unstable, zProviderConfigAuthenticateRequest_unstable, + zProviderSecretsListRequest_unstable, + zProviderSecretDeleteRequest_unstable, + zCanonicalModelInfoRequest_unstable, zPreferencesReadRequest_unstable, zPreferencesSaveRequest_unstable, zPreferencesRemoveRequest_unstable, + zConfigReadRequest_unstable, + zConfigUpsertRequest_unstable, + zConfigRemoveRequest_unstable, + zConfigReadAllRequest_unstable, zDefaultsReadRequest_unstable, zDefaultsSaveRequest_unstable, + zDefaultsClearRequest_unstable, zOnboardingImportScanRequest_unstable, zOnboardingImportApplyRequest_unstable, zExportSessionRequest_unstable, @@ -2264,6 +2497,7 @@ export const zExtResponse = z.union([ z.union([ zEmptyResponse, zGetToolsResponse_unstable, + zSetToolPermissionsResponse_unstable, zGooseToolCallResponse_unstable, zReadResourceResponse_unstable, zAppsListResponse_unstable, @@ -2271,6 +2505,9 @@ export const zExtResponse = z.union([ zAppsImportResponse_unstable, zSteerSessionResponse_unstable, zDiagnosticsGetResponse_unstable, + zListPromptsResponse_unstable, + zGetPromptResponse_unstable, + zPromptOperationResponse_unstable, zGetConfigExtensionsResponse_unstable, zGetAvailableExtensionsResponse_unstable, zGetSessionExtensionsResponse_unstable, @@ -2287,7 +2524,11 @@ export const zExtResponse = z.union([ zProviderConfigReadResponse_unstable, zProviderConfigStatusResponse_unstable, zProviderConfigChangeResponse_unstable, + zProviderSecretsListResponse_unstable, + zCanonicalModelInfoResponse_unstable, zPreferencesReadResponse_unstable, + zConfigReadResponse_unstable, + zConfigReadAllResponse_unstable, zDefaultsReadResponse_unstable, zOnboardingImportScanResponse_unstable, zOnboardingImportApplyResponse_unstable,