From ccaf7ca56d05ed76c5ce23c2ccb7a7fe3ade0e30 Mon Sep 17 00:00:00 2001 From: Blair Allan <72862871+BlairAllan@users.noreply.github.com> Date: Mon, 1 Dec 2025 21:01:03 +0000 Subject: [PATCH] Feat: Added custom headers and toggle keyring CLI options (#5017) Signed-off-by: Blair Allan Co-authored-by: dianed-square <73617011+dianed-square@users.noreply.github.com> --- crates/goose-cli/src/commands/configure.rs | 117 +++++++++++++++++- .../src/routes/config_management.rs | 2 + .../goose/src/config/declarative_providers.rs | 3 +- ui/desktop/openapi.json | 7 ++ ui/desktop/src/api/types.gen.ts | 3 + 5 files changed, 129 insertions(+), 3 deletions(-) diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index dd7ce318e5..e50dd6f09f 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -1174,6 +1174,11 @@ pub async fn configure_settings_dialog() -> anyhow::Result<()> { "Max Turns", "Set maximum number of turns without user input", ) + .item( + "keyring", + "Secret Storage", + "Configure how secrets are stored (keyring vs file)", + ) .item( "experiment", "Toggle Experiment", @@ -1206,6 +1211,9 @@ pub async fn configure_settings_dialog() -> anyhow::Result<()> { "max_turns" => { configure_max_turns_dialog()?; } + "keyring" => { + configure_keyring_dialog()?; + } "experiment" => { toggle_experiments_dialog()?; } @@ -1225,7 +1233,6 @@ pub async fn configure_settings_dialog() -> anyhow::Result<()> { pub fn configure_goose_mode_dialog() -> anyhow::Result<()> { let config = Config::global(); - // Check if GOOSE_MODE is set as an environment variable if std::env::var("GOOSE_MODE").is_ok() { let _ = cliclack::log::info("Notice: GOOSE_MODE environment variable is set and will override the configuration here."); } @@ -1293,7 +1300,7 @@ pub fn configure_goose_router_strategy_dialog() -> anyhow::Result<()> { pub fn configure_tool_output_dialog() -> anyhow::Result<()> { let config = Config::global(); - // Check if GOOSE_CLI_MIN_PRIORITY is set as an environment variable + if std::env::var("GOOSE_CLI_MIN_PRIORITY").is_ok() { let _ = cliclack::log::info("Notice: GOOSE_CLI_MIN_PRIORITY environment variable is set and will override the configuration here."); } @@ -1322,6 +1329,60 @@ pub fn configure_tool_output_dialog() -> anyhow::Result<()> { Ok(()) } +pub fn configure_keyring_dialog() -> anyhow::Result<()> { + let config = Config::global(); + + if std::env::var("GOOSE_DISABLE_KEYRING").is_ok() { + let _ = cliclack::log::info("Notice: GOOSE_DISABLE_KEYRING environment variable is set and will override the configuration here."); + } + + let currently_disabled = config.get_param::("GOOSE_DISABLE_KEYRING").is_ok(); + + let current_status = if currently_disabled { + "Disabled (using file-based storage)" + } else { + "Enabled (using system keyring)" + }; + + let _ = cliclack::log::info(format!("Current secret storage: {}", current_status)); + let _ = cliclack::log::warning("Note: Disabling the keyring stores secrets in a plain text file (~/.config/goose/secrets.yaml)"); + + let storage_option = cliclack::select("How would you like to store secrets?") + .item( + "keyring", + "System Keyring (recommended)", + "Use secure system keyring for storing API keys and secrets", + ) + .item( + "file", + "File-based Storage", + "Store secrets in a local file (useful when keyring access is restricted)", + ) + .interact()?; + + match storage_option { + "keyring" => { + // Set to empty string to enable keyring (absence or empty = enabled) + config.set_param("GOOSE_DISABLE_KEYRING", Value::String("".to_string()))?; + cliclack::outro("Secret storage set to system keyring (secure)")?; + let _ = + cliclack::log::info("You may need to restart goose for this change to take effect"); + } + "file" => { + // Set the disable flag to use file storage + config.set_param("GOOSE_DISABLE_KEYRING", Value::String("true".to_string()))?; + cliclack::outro( + "Secret storage set to file (~/.config/goose/secrets.yaml). Keep this file secure!", + )?; + let _ = + cliclack::log::info("You may need to restart goose for this change to take effect"); + } + _ => unreachable!(), + }; + + Ok(()) +} + /// Configure experiment features that can be used with goose /// Dialog for toggling which experiments are enabled/disabled pub fn toggle_experiments_dialog() -> anyhow::Result<()> { @@ -1740,6 +1801,50 @@ pub async fn handle_tetrate_auth() -> anyhow::Result<()> { Ok(()) } +/// Prompts the user to collect custom HTTP headers for a provider. +fn collect_custom_headers() -> anyhow::Result>> { + let use_custom_headers = cliclack::confirm("Does this provider require custom headers?") + .initial_value(false) + .interact()?; + + if !use_custom_headers { + return Ok(None); + } + + let mut custom_headers = std::collections::HashMap::new(); + + loop { + let header_name: String = cliclack::input("Header name:") + .placeholder("e.g., x-origin-client-id") + .required(false) + .interact()?; + + if header_name.is_empty() { + break; + } + + let header_value: String = cliclack::password(format!("Value for '{}':", header_name)) + .mask('▪') + .interact()?; + + custom_headers.insert(header_name, header_value); + + let add_more = cliclack::confirm("Add another header?") + .initial_value(false) + .interact()?; + + if !add_more { + break; + } + } + + if custom_headers.is_empty() { + Ok(None) + } else { + Ok(Some(custom_headers)) + } +} + fn add_provider() -> anyhow::Result<()> { let provider_type = cliclack::select("What type of API is this?") .item( @@ -1807,6 +1912,13 @@ fn add_provider() -> anyhow::Result<()> { .initial_value(true) .interact()?; + // Ask about custom headers for OpenAI compatible providers + let headers = if provider_type == "openai_compatible" { + collect_custom_headers()? + } else { + None + }; + create_custom_provider( provider_type, display_name.clone(), @@ -1814,6 +1926,7 @@ fn add_provider() -> anyhow::Result<()> { api_key, models, Some(supports_streaming), + headers, )?; cliclack::outro(format!("Custom provider added: {}", display_name))?; diff --git a/crates/goose-server/src/routes/config_management.rs b/crates/goose-server/src/routes/config_management.rs index 7130b4647d..5e2001af4b 100644 --- a/crates/goose-server/src/routes/config_management.rs +++ b/crates/goose-server/src/routes/config_management.rs @@ -87,6 +87,7 @@ pub struct UpdateCustomProviderRequest { pub api_key: String, pub models: Vec, pub supports_streaming: Option, + pub headers: Option>, } #[derive(Deserialize, ToSchema)] @@ -706,6 +707,7 @@ pub async fn create_custom_provider( request.api_key, request.models, request.supports_streaming, + request.headers, ) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; diff --git a/crates/goose/src/config/declarative_providers.rs b/crates/goose/src/config/declarative_providers.rs index a063ec4756..273612f949 100644 --- a/crates/goose/src/config/declarative_providers.rs +++ b/crates/goose/src/config/declarative_providers.rs @@ -92,6 +92,7 @@ pub fn create_custom_provider( api_key: String, models: Vec, supports_streaming: Option, + headers: Option>, ) -> Result { let id = generate_id(&display_name); let api_key_name = generate_api_key_name(&id); @@ -117,7 +118,7 @@ pub fn create_custom_provider( api_key_env: api_key_name, base_url: api_url, models: model_infos, - headers: None, + headers, timeout_seconds: None, supports_streaming, }; diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 769f2fa747..564470494f 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -5275,6 +5275,13 @@ "engine": { "type": "string" }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "nullable": true + }, "models": { "type": "array", "items": { diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index ac44b284c5..f4536a91a1 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -935,6 +935,9 @@ export type UpdateCustomProviderRequest = { api_url: string; display_name: string; engine: string; + headers?: { + [key: string]: string; + } | null; models: Array; supports_streaming?: boolean | null; };