From e44a0cdc1a72cd52b3fc564d04b96cb019c4d548 Mon Sep 17 00:00:00 2001 From: Will Pfleger Date: Fri, 17 Apr 2026 15:07:46 -0400 Subject: [PATCH] feat: add JSON Schema for config.yaml with typed config accessors Goose has no machine-readable schema for config.yaml and config keys were accessed via untyped string literals across the codebase. This adds a JSON Schema for IDE autocomplete and validation, with GooseConfigSchema as the authoritative registry for user-facing config keys. Typed accessors replace raw string-key reads across user-facing config call sites while compile-time assertions keep accessor declarations tied to schema membership. Raw get_param and set_param remain available for dynamic and internal config storage. Signed-off-by: Will Pfleger --- .github/workflows/ci.yml | 5 + Justfile | 22 + crates/goose-cli/src/commands/configure.rs | 36 +- crates/goose-cli/src/recipes/github_recipe.rs | 1 - crates/goose-cli/src/recipes/search_recipe.rs | 3 +- crates/goose-cli/src/session/builder.rs | 27 +- crates/goose-cli/src/session/input.rs | 2 +- crates/goose-cli/src/session/mod.rs | 12 +- crates/goose-cli/src/session/output.rs | 20 +- crates/goose-server/src/tunnel/mod.rs | 6 +- crates/goose/Cargo.toml | 4 + crates/goose/config.schema.json | 1428 +++++++++++++++++ crates/goose/src/acp/server/config.rs | 7 +- crates/goose/src/agents/agent.rs | 9 +- crates/goose/src/agents/extension.rs | 5 +- .../platform_extensions/code_execution.rs | 2 +- .../src/agents/platform_extensions/summon.rs | 16 +- crates/goose/src/agents/retry.rs | 12 +- .../goose/src/agents/subagent_task_config.rs | 2 +- .../goose/src/bin/generate_config_schema.rs | 13 + crates/goose/src/config/base.rs | 134 ++ crates/goose/src/config/extensions.rs | 3 +- crates/goose/src/config/goose_mode.rs | 2 + crates/goose/src/config/mod.rs | 1 + crates/goose/src/config/schema.rs | 476 ++++++ crates/goose/src/context_mgmt/mod.rs | 4 +- crates/goose/src/dictation/providers.rs | 4 +- crates/goose/src/hints/load_hints.rs | 2 +- crates/goose/src/model.rs | 4 +- crates/goose/src/otel/otlp.rs | 4 +- crates/goose/src/posthog.rs | 19 +- crates/goose/src/providers/anthropic.rs | 4 +- crates/goose/src/providers/api_client.rs | 6 +- crates/goose/src/providers/avian.rs | 2 +- crates/goose/src/providers/azure.rs | 6 +- crates/goose/src/providers/bedrock.rs | 16 +- crates/goose/src/providers/databricks.rs | 12 +- crates/goose/src/providers/gcpvertexai.rs | 12 +- crates/goose/src/providers/githubcopilot.rs | 8 +- crates/goose/src/providers/google.rs | 4 +- crates/goose/src/providers/litellm.rs | 6 +- crates/goose/src/providers/ollama.rs | 18 +- crates/goose/src/providers/openai.rs | 62 +- crates/goose/src/providers/openrouter.rs | 2 +- crates/goose/src/providers/sagemaker_tgi.rs | 2 +- crates/goose/src/providers/snowflake.rs | 2 +- crates/goose/src/providers/tetrate.rs | 2 +- crates/goose/src/providers/venice.rs | 6 +- crates/goose/src/providers/xai.rs | 2 +- crates/goose/src/security/mod.rs | 12 +- crates/goose/src/security/scanner.rs | 2 +- crates/goose/src/slash_commands.rs | 3 +- 52 files changed, 2275 insertions(+), 199 deletions(-) create mode 100644 crates/goose/config.schema.json create mode 100644 crates/goose/src/bin/generate_config_schema.rs create mode 100644 crates/goose/src/config/schema.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 266d99271545..c30772d90b18 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -194,6 +194,11 @@ jobs: source ./bin/activate-hermit just check-acp-schema + - name: Check Config Schema is Up-to-Date + run: | + source ./bin/activate-hermit + just check-config-schema + desktop-lint: name: Test and Lint Electron Desktop App runs-on: macos-latest diff --git a/Justfile b/Justfile index 3e7a22092964..7a1471624d80 100644 --- a/Justfile +++ b/Justfile @@ -17,6 +17,8 @@ check-everything: cd ui/desktop && pnpm run lint:check @echo " → Validating OpenAPI schema..." ./scripts/check-openapi-schema.sh + @echo " → Validating config schema..." + just check-config-schema @echo "" @echo "✅ All style checks passed!" @@ -226,6 +228,26 @@ generate-acp-types: generate-acp-schema cd ui/sdk && npx tsx generate-schema.ts @echo "ACP TypeScript types generated in ui/sdk/src/generated/" +# Generate config.yaml JSON schema from Rust types +generate-config-schema: + @echo "Generating config.yaml JSON schema..." + cargo run -p goose --bin generate_config_schema + @echo "Config schema generated: crates/goose/config.schema.json" + +# Check if config.yaml JSON schema is up-to-date +check-config-schema: generate-config-schema + #!/usr/bin/env bash + set -e + echo "Checking config schema is up-to-date..." + if ! git diff --exit-code crates/goose/config.schema.json; then + echo "" + echo "Config schema is out of date!" + echo "" + echo "Run 'just generate-config-schema' locally, then commit the changes." + exit 1 + fi + echo "Config schema is up-to-date" + # Build SDK TypeScript package (schema + types + compile) build-sdk: generate-acp-types @echo "Compiling ACP TypeScript..." diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index c682724276cd..1b492e3a4410 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -1,4 +1,3 @@ -use crate::recipes::github_recipe::GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY; use cliclack::spinner; use console::style; use goose::agents::extension::{ToolInfo, PLATFORM_EXTENSIONS}; @@ -21,14 +20,13 @@ use goose::config::{ }; use goose::model::ModelConfig; #[cfg(feature = "telemetry")] -use goose::posthog::{get_telemetry_choice, TELEMETRY_ENABLED_KEY}; +use goose::posthog::get_telemetry_choice; use goose::providers::base::ConfigKey; use goose::providers::chatgpt_codex::reasoning_levels_for_model; use goose::providers::formats::anthropic::supports_adaptive_thinking; use goose::providers::provider_test::test_provider_configuration; use goose::providers::{create, providers, retry_operation, RetryConfig}; use goose::session::SessionType; -use serde_json::Value; use std::collections::HashMap; // useful for light themes where there is no discernible colour contrast between @@ -96,7 +94,7 @@ pub fn configure_telemetry_consent_dialog() -> anyhow::Result { .initial_value(true) .interact()?; - config.set_param(TELEMETRY_ENABLED_KEY, enabled)?; + config.set_goose_telemetry_enabled(enabled)?; if enabled { let _ = cliclack::log::success("Thank you for helping improve goose!"); @@ -1429,7 +1427,7 @@ pub fn configure_telemetry_dialog() -> anyhow::Result<()> { .initial_value(current_choice.unwrap_or(true)) .interact()?; - config.set_param(TELEMETRY_ENABLED_KEY, enabled)?; + config.set_goose_telemetry_enabled(enabled)?; if enabled { cliclack::outro("Telemetry enabled - thank you for helping improve goose!")?; @@ -1456,15 +1454,15 @@ pub fn configure_tool_output_dialog() -> anyhow::Result<()> { match tool_log_level { "high" => { - config.set_param("GOOSE_CLI_MIN_PRIORITY", 0.8)?; + config.set_goose_cli_min_priority(0.8)?; cliclack::outro("Showing tool output of high importance only.")?; } "medium" => { - config.set_param("GOOSE_CLI_MIN_PRIORITY", 0.2)?; + config.set_goose_cli_min_priority(0.2)?; cliclack::outro("Showing tool output of medium importance.")?; } "all" => { - config.set_param("GOOSE_CLI_MIN_PRIORITY", 0.0)?; + config.set_goose_cli_min_priority(0.0)?; cliclack::outro("Showing all tool output.")?; } _ => unreachable!(), @@ -1482,7 +1480,10 @@ pub fn configure_keyring_dialog() -> anyhow::Result<()> { ); } - let currently_disabled = config.get_param::("GOOSE_DISABLE_KEYRING").is_ok(); + let currently_disabled = config + .get_goose_disable_keyring() + .ok() + .is_some_and(|value| matches!(value.as_str(), "1") || value.eq_ignore_ascii_case("true")); let current_status = if currently_disabled { "Disabled (using file-based storage)" @@ -1513,14 +1514,14 @@ pub fn configure_keyring_dialog() -> anyhow::Result<()> { 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()))?; + config.set_goose_disable_keyring("".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()))?; + config.set_goose_disable_keyring("true".to_string())?; cliclack::outro(format!( "Secret storage set to file ({}). Keep this file secure!", secrets_path.display(), @@ -1745,11 +1746,10 @@ pub async fn configure_tool_permissions_dialog() -> anyhow::Result<()> { } fn configure_recipe_dialog() -> anyhow::Result<()> { - let key_name = GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY; let config = Config::global(); - let default_recipe_repo = std::env::var(key_name) + let default_recipe_repo = std::env::var("GOOSE_RECIPE_GITHUB_REPO") .ok() - .or_else(|| config.get_param(key_name).unwrap_or(None)); + .or_else(|| config.get_goose_recipe_github_repo().unwrap_or(None)); let mut recipe_repo_input = cliclack::input( "Enter your goose recipe GitHub repo (owner/repo): eg: my_org/goose-recipes", ) @@ -1759,9 +1759,9 @@ fn configure_recipe_dialog() -> anyhow::Result<()> { } let input_value: String = recipe_repo_input.interact()?; if input_value.clone().trim().is_empty() { - config.delete(key_name)?; + config.delete("GOOSE_RECIPE_GITHUB_REPO")?; } else { - config.set_param(key_name, &input_value)?; + config.set_goose_recipe_github_repo(Some(input_value))?; } Ok(()) } @@ -1769,7 +1769,7 @@ fn configure_recipe_dialog() -> anyhow::Result<()> { pub fn configure_max_turns_dialog() -> anyhow::Result<()> { let config = Config::global(); - let current_max_turns: u32 = config.get_param("GOOSE_MAX_TURNS").unwrap_or(1000); + let current_max_turns: u32 = config.get_goose_max_turns().unwrap_or(1000); let max_turns_input: String = cliclack::input("Set maximum number of agent turns without user input:") @@ -1788,7 +1788,7 @@ pub fn configure_max_turns_dialog() -> anyhow::Result<()> { .interact()?; let max_turns: u32 = max_turns_input.parse()?; - config.set_param("GOOSE_MAX_TURNS", max_turns)?; + config.set_goose_max_turns(max_turns)?; cliclack::outro(format!( "Set maximum turns to {} - goose will ask for input after {} consecutive actions", diff --git a/crates/goose-cli/src/recipes/github_recipe.rs b/crates/goose-cli/src/recipes/github_recipe.rs index f19c3a60d882..42b105f4a3a3 100644 --- a/crates/goose-cli/src/recipes/github_recipe.rs +++ b/crates/goose-cli/src/recipes/github_recipe.rs @@ -30,7 +30,6 @@ pub enum RecipeSource { GitHub, } -pub const GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY: &str = "GOOSE_RECIPE_GITHUB_REPO"; pub fn retrieve_recipe_from_github( recipe_name: &str, recipe_repo_full_name: &str, diff --git a/crates/goose-cli/src/recipes/search_recipe.rs b/crates/goose-cli/src/recipes/search_recipe.rs index 0dd9a5255683..cd7efff2a9b2 100644 --- a/crates/goose-cli/src/recipes/search_recipe.rs +++ b/crates/goose-cli/src/recipes/search_recipe.rs @@ -4,7 +4,6 @@ use goose::recipe::read_recipe_file_content::RecipeFile; use super::github_recipe::{ list_github_recipes, retrieve_recipe_from_github, RecipeInfo, RecipeSource, - GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY, }; use goose::recipe::local_recipes::{list_local_recipes, load_local_recipe_file}; @@ -20,7 +19,7 @@ pub fn load_recipe_file(recipe_name: &str) -> Result { fn configured_github_recipe_repo() -> Option { let config = Config::global(); - match config.get_param(GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY) { + match config.get_goose_recipe_github_repo() { Ok(Some(recipe_repo_full_name)) => Some(recipe_repo_full_name), _ => None, } diff --git a/crates/goose-cli/src/session/builder.rs b/crates/goose-cli/src/session/builder.rs index 0cd8e3a47e1a..810bdf8039a2 100644 --- a/crates/goose-cli/src/session/builder.rs +++ b/crates/goose-cli/src/session/builder.rs @@ -466,7 +466,7 @@ async fn configure_session_prompts( .await; } - let system_prompt_file: Option = config.get_param("GOOSE_SYSTEM_PROMPT_FILE_PATH").ok(); + let system_prompt_file: Option = config.get_goose_system_prompt_file_path().ok(); if let Some(ref path) = system_prompt_file { let override_prompt = std::fs::read_to_string(path).unwrap_or_else(|e| { output::render_error(&format!( @@ -581,19 +581,20 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { // Extensions are loaded after session creation because we may change directory when resuming let agent_ptr = resolve_and_load_extensions(agent, extensions_for_provider, &session_id).await; - let edit_mode = config - .get_param::("EDIT_MODE") - .ok() - .and_then(|edit_mode| match edit_mode.to_lowercase().as_str() { - "emacs" => Some(EditMode::Emacs), - "vi" => Some(EditMode::Vi), - _ => { - eprintln!("Invalid EDIT_MODE specified, defaulting to Emacs"); - None - } - }); + let edit_mode = + config + .get_edit_mode() + .ok() + .and_then(|edit_mode| match edit_mode.to_lowercase().as_str() { + "emacs" => Some(EditMode::Emacs), + "vi" => Some(EditMode::Vi), + _ => { + eprintln!("Invalid EDIT_MODE specified, defaulting to Emacs"); + None + } + }); - let debug_mode = session_config.debug || config.get_param("GOOSE_DEBUG").unwrap_or(false); + let debug_mode = session_config.debug || config.get_goose_debug().unwrap_or(false); let session = CliSession::new( Arc::try_unwrap(agent_ptr).unwrap_or_else(|_| panic!("There should be no more references")), diff --git a/crates/goose-cli/src/session/input.rs b/crates/goose-cli/src/session/input.rs index 2641a77b725e..36428c6fb513 100644 --- a/crates/goose-cli/src/session/input.rs +++ b/crates/goose-cli/src/session/input.rs @@ -84,7 +84,7 @@ impl rustyline::ConditionalEventHandler for CtrlCHandler { pub fn get_newline_key() -> char { Config::global() - .get_param::("GOOSE_CLI_NEWLINE_KEY") + .get_goose_cli_newline_key() .ok() .and_then(|s| s.chars().next()) .map(|c| c.to_ascii_lowercase()) diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index 4deb36ae3d14..dbd721d294e5 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -1455,9 +1455,7 @@ impl CliSession { let context_limit = model_config.context_limit(); let config = Config::global(); - let show_cost = config - .get_param::("GOOSE_CLI_SHOW_COST") - .unwrap_or(false); + let show_cost = config.get_goose_cli_show_cost().unwrap_or(false); let provider_name = config .get_goose_provider() @@ -1850,7 +1848,7 @@ fn format_logging_notification( Some("response_generated") => { let config = Config::global(); let min_priority = config - .get_param::("GOOSE_CLI_MIN_PRIORITY") + .get_goose_cli_min_priority() .ok() .unwrap_or(output::DEFAULT_MIN_PRIORITY); @@ -1916,7 +1914,7 @@ fn display_log_notification( } else if ntype == "shell_output" { let config = Config::global(); let min_priority = config - .get_param::("GOOSE_CLI_MIN_PRIORITY") + .get_goose_cli_min_priority() .ok() .unwrap_or(output::DEFAULT_MIN_PRIORITY); @@ -2023,7 +2021,7 @@ async fn get_reasoner() -> Result, anyhow::Error> { let config = Config::global(); // Try planner-specific provider first, fall back to default provider - let provider = if let Ok(provider) = config.get_param::("GOOSE_PLANNER_PROVIDER") { + let provider = if let Ok(provider) = config.get_goose_planner_provider() { provider } else { println!("WARNING: GOOSE_PLANNER_PROVIDER not found. Using default provider..."); @@ -2033,7 +2031,7 @@ async fn get_reasoner() -> Result, anyhow::Error> { }; // Try planner-specific model first, fall back to default model - let model = if let Ok(model) = config.get_param::("GOOSE_PLANNER_MODEL") { + let model = if let Ok(model) = config.get_goose_planner_model() { model } else { println!("WARNING: GOOSE_PLANNER_MODEL not found. Using default model..."); diff --git a/crates/goose-cli/src/session/output.rs b/crates/goose-cli/src/session/output.rs index b0c71e2b4212..481257655502 100644 --- a/crates/goose-cli/src/session/output.rs +++ b/crates/goose-cli/src/session/output.rs @@ -37,10 +37,10 @@ impl Theme { fn as_str(&self) -> String { match self { Theme::Light => Config::global() - .get_param::("GOOSE_CLI_LIGHT_THEME") + .get_goose_cli_light_theme() .unwrap_or(DEFAULT_CLI_LIGHT_THEME.to_string()), Theme::Dark => Config::global() - .get_param::("GOOSE_CLI_DARK_THEME") + .get_goose_cli_dark_theme() .unwrap_or(DEFAULT_CLI_DARK_THEME.to_string()), Theme::Ansi => "base16".to_string(), } @@ -70,20 +70,20 @@ thread_local! { std::env::var("GOOSE_CLI_THEME").ok() .map(|val| Theme::from_config_str(&val)) .unwrap_or_else(|| - Config::global().get_param::("GOOSE_CLI_THEME").ok() + Config::global().get_goose_cli_theme().ok() .map(|val| Theme::from_config_str(&val)) .unwrap_or(Theme::Ansi) ) ); static SHOW_FULL_TOOL_OUTPUT: RefCell = RefCell::new( - Config::global().get_param::("GOOSE_SHOW_FULL_OUTPUT").unwrap_or(false) + Config::global().get_goose_show_full_output().unwrap_or(false) ); } pub fn set_theme(theme: Theme) { let config = Config::global(); config - .set_param("GOOSE_CLI_THEME", theme.as_config_string()) + .set_goose_cli_theme(theme.as_config_string()) .expect("Failed to set theme"); CURRENT_THEME.with(|t| *t.borrow_mut() = theme); @@ -94,7 +94,7 @@ pub fn set_theme(theme: Theme) { Theme::Ansi => "ansi", }; - if let Err(e) = config.set_param("GOOSE_CLI_THEME", theme_str) { + if let Err(e) = config.set_goose_cli_theme(theme_str) { eprintln!("Failed to save theme setting to config: {}", e); } } @@ -126,7 +126,7 @@ impl ThinkingIndicator { let spinner = cliclack::spinner(); let hint = style("(Ctrl+C to interrupt)").dim(); if Config::global() - .get_param("RANDOM_THINKING_MESSAGES") + .get_random_thinking_messages() .unwrap_or(true) { spinner.start(format!( @@ -177,7 +177,7 @@ pub fn hide_thinking() { } pub fn run_status_hook(status: &str) { - if let Ok(hook) = Config::global().get_param::("GOOSE_STATUS_HOOK") { + if let Ok(hook) = Config::global().get_goose_status_hook() { let status = status.to_string(); std::thread::spawn(move || { #[cfg(target_os = "windows")] @@ -449,7 +449,7 @@ pub fn goose_mode_message(text: &str) { fn should_show_thinking() -> bool { Config::global() - .get_param::("GOOSE_CLI_SHOW_THINKING") + .get_goose_cli_show_thinking() .unwrap_or(false) && std::io::stdout().is_terminal() } @@ -507,7 +507,7 @@ fn render_tool_response(resp: &ToolResponse, debug: bool) { } let min_priority = config - .get_param::("GOOSE_CLI_MIN_PRIORITY") + .get_goose_cli_min_priority() .ok() .unwrap_or(DEFAULT_MIN_PRIORITY); diff --git a/crates/goose-server/src/tunnel/mod.rs b/crates/goose-server/src/tunnel/mod.rs index 4e75f0304538..894dc9e022e1 100644 --- a/crates/goose-server/src/tunnel/mod.rs +++ b/crates/goose-server/src/tunnel/mod.rs @@ -113,9 +113,7 @@ impl TunnelManager { } fn get_auto_start() -> bool { - Config::global() - .get_param("tunnel_auto_start") - .unwrap_or(false) + Config::global().get_tunnel_auto_start().unwrap_or(false) } fn get_secret() -> Option { @@ -200,7 +198,7 @@ impl TunnelManager { pub fn set_auto_start(auto_start: bool) -> anyhow::Result<()> { Config::global() - .set_param("tunnel_auto_start", auto_start) + .set_tunnel_auto_start(auto_start) .map_err(|e| anyhow::anyhow!("Failed to save tunnel config: {}", e)) } diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml index 658626e6d46b..f78143361906 100644 --- a/crates/goose/Cargo.toml +++ b/crates/goose/Cargo.toml @@ -253,6 +253,10 @@ path = "src/providers/canonical/build_canonical_models.rs" name = "generate-acp-schema" path = "src/bin/generate_acp_schema.rs" +[[bin]] +name = "generate_config_schema" +path = "src/bin/generate_config_schema.rs" + [package.metadata.cargo-machete] ignored = [ diff --git a/crates/goose/config.schema.json b/crates/goose/config.schema.json new file mode 100644 index 000000000000..c3318af699a6 --- /dev/null +++ b/crates/goose/config.schema.json @@ -0,0 +1,1428 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "GooseConfigSchema", + "description": "JSON Schema representation of Goose's config.yaml.\n\nAll keys are optional. Unknown keys are allowed (additionalProperties: true)\nbecause Goose passes undocumented provider-specific keys through as\nenvironment variable overrides.", + "type": "object", + "properties": { + "GOOSE_PROVIDER": { + "type": [ + "string", + "null" + ] + }, + "GOOSE_MODEL": { + "type": [ + "string", + "null" + ] + }, + "GOOSE_MODE": { + "anyOf": [ + { + "$ref": "#/$defs/GooseMode" + }, + { + "type": "null" + } + ] + }, + "GOOSE_MAX_TOKENS": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "GOOSE_CONTEXT_LIMIT": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "GOOSE_INPUT_LIMIT": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "GOOSE_MAX_TURNS": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0 + }, + "GOOSE_MAX_ACTIVE_AGENTS": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "GOOSE_AUTO_COMPACT_THRESHOLD": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "GOOSE_TOOL_PAIR_SUMMARIZATION": { + "type": [ + "boolean", + "null" + ] + }, + "GOOSE_TOOL_CALL_CUTOFF": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "GOOSE_STREAM_TIMEOUT": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "GOOSE_SEARCH_PATHS": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "GOOSE_DISABLE_SESSION_NAMING": { + "type": [ + "boolean", + "null" + ] + }, + "GOOSE_DISABLE_KEYRING": { + "type": [ + "string", + "null" + ] + }, + "GOOSE_TELEMETRY_ENABLED": { + "type": [ + "boolean", + "null" + ] + }, + "GOOSE_DEFAULT_EXTENSION_TIMEOUT": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "GOOSE_PROMPT_EDITOR": { + "type": [ + "string", + "null" + ] + }, + "GOOSE_PROMPT_EDITOR_ALWAYS": { + "type": [ + "boolean", + "null" + ] + }, + "GOOSE_ALLOWLIST": { + "type": [ + "string", + "null" + ] + }, + "GOOSE_SYSTEM_PROMPT_FILE_PATH": { + "type": [ + "string", + "null" + ] + }, + "GOOSE_DEBUG": { + "type": [ + "boolean", + "null" + ] + }, + "GOOSE_SHOW_FULL_OUTPUT": { + "type": [ + "boolean", + "null" + ] + }, + "GOOSE_STATUS_HOOK": { + "type": [ + "string", + "null" + ] + }, + "GOOSE_LOCAL_ENABLE_THINKING": { + "type": [ + "boolean", + "null" + ] + }, + "GOOSE_DATABRICKS_CLIENT_REQUEST_ID": { + "type": [ + "boolean", + "null" + ] + }, + "CONTEXT_FILE_NAMES": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "EDIT_MODE": { + "type": [ + "string", + "null" + ] + }, + "RANDOM_THINKING_MESSAGES": { + "type": [ + "boolean", + "null" + ] + }, + "CODE_MODE_TOOL_DISCLOSURE": { + "type": [ + "string", + "null" + ] + }, + "GOOSE_CLIENT_CERT_PATH": { + "type": [ + "string", + "null" + ] + }, + "GOOSE_CLIENT_KEY_PATH": { + "type": [ + "string", + "null" + ] + }, + "GOOSE_CA_CERT_PATH": { + "type": [ + "string", + "null" + ] + }, + "GOOSE_PLANNER_PROVIDER": { + "type": [ + "string", + "null" + ] + }, + "GOOSE_PLANNER_MODEL": { + "type": [ + "string", + "null" + ] + }, + "GOOSE_SUBAGENT_PROVIDER": { + "type": [ + "string", + "null" + ] + }, + "GOOSE_SUBAGENT_MODEL": { + "type": [ + "string", + "null" + ] + }, + "GOOSE_SUBAGENT_MAX_TURNS": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "GOOSE_MAX_BACKGROUND_TASKS": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "GOOSE_RECIPE_GITHUB_REPO": { + "type": [ + "string", + "null" + ] + }, + "GOOSE_RECIPE_RETRY_TIMEOUT_SECONDS": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "GOOSE_RECIPE_ON_FAILURE_TIMEOUT_SECONDS": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "GOOSE_CLI_MIN_PRIORITY": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "GOOSE_CLI_THEME": { + "type": [ + "string", + "null" + ] + }, + "GOOSE_CLI_LIGHT_THEME": { + "type": [ + "string", + "null" + ] + }, + "GOOSE_CLI_DARK_THEME": { + "type": [ + "string", + "null" + ] + }, + "GOOSE_CLI_SHOW_COST": { + "type": [ + "boolean", + "null" + ] + }, + "GOOSE_CLI_SHOW_THINKING": { + "type": [ + "boolean", + "null" + ] + }, + "GOOSE_CLI_NEWLINE_KEY": { + "type": [ + "string", + "null" + ] + }, + "CLAUDE_CODE_COMMAND": { + "type": [ + "string", + "null" + ] + }, + "GEMINI_CLI_COMMAND": { + "type": [ + "string", + "null" + ] + }, + "CURSOR_AGENT_COMMAND": { + "type": [ + "string", + "null" + ] + }, + "CODEX_COMMAND": { + "type": [ + "string", + "null" + ] + }, + "CODEX_REASONING_EFFORT": { + "type": [ + "string", + "null" + ] + }, + "CODEX_ENABLE_SKILLS": { + "type": [ + "string", + "null" + ] + }, + "CODEX_SKIP_GIT_CHECK": { + "type": [ + "string", + "null" + ] + }, + "CHATGPT_CODEX_REASONING_EFFORT": { + "type": [ + "string", + "null" + ] + }, + "CLAUDE_THINKING_TYPE": { + "type": [ + "string", + "null" + ] + }, + "CLAUDE_THINKING_EFFORT": { + "type": [ + "string", + "null" + ] + }, + "CLAUDE_THINKING_BUDGET": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "GEMINI3_THINKING_LEVEL": { + "type": [ + "string", + "null" + ] + }, + "GEMINI25_THINKING_BUDGET": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "SECURITY_PROMPT_ENABLED": { + "type": [ + "boolean", + "null" + ] + }, + "SECURITY_PROMPT_THRESHOLD": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "SECURITY_PROMPT_CLASSIFIER_ENABLED": { + "type": [ + "boolean", + "null" + ] + }, + "SECURITY_PROMPT_CLASSIFIER_MODEL": { + "type": [ + "string", + "null" + ] + }, + "SECURITY_PROMPT_CLASSIFIER_ENDPOINT": { + "type": [ + "string", + "null" + ] + }, + "SECURITY_COMMAND_CLASSIFIER_ENABLED": { + "type": [ + "boolean", + "null" + ] + }, + "OPENAI_HOST": { + "type": [ + "string", + "null" + ] + }, + "OPENAI_BASE_URL": { + "type": [ + "string", + "null" + ] + }, + "OPENAI_BASE_PATH": { + "type": [ + "string", + "null" + ] + }, + "OPENAI_ORGANIZATION": { + "type": [ + "string", + "null" + ] + }, + "OPENAI_PROJECT": { + "type": [ + "string", + "null" + ] + }, + "OPENAI_TIMEOUT": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "ANTHROPIC_HOST": { + "type": [ + "string", + "null" + ] + }, + "OLLAMA_HOST": { + "type": [ + "string", + "null" + ] + }, + "OLLAMA_TIMEOUT": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "OLLAMA_STREAM_TIMEOUT": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "OLLAMA_STREAM_USAGE": { + "type": [ + "boolean", + "null" + ] + }, + "DATABRICKS_HOST": { + "type": [ + "string", + "null" + ] + }, + "DATABRICKS_MAX_RETRIES": { + "type": [ + "string", + "null" + ] + }, + "DATABRICKS_INITIAL_RETRY_INTERVAL_MS": { + "type": [ + "string", + "null" + ] + }, + "DATABRICKS_BACKOFF_MULTIPLIER": { + "type": [ + "string", + "null" + ] + }, + "DATABRICKS_MAX_RETRY_INTERVAL_MS": { + "type": [ + "string", + "null" + ] + }, + "AZURE_OPENAI_ENDPOINT": { + "type": [ + "string", + "null" + ] + }, + "AZURE_OPENAI_DEPLOYMENT_NAME": { + "type": [ + "string", + "null" + ] + }, + "AZURE_OPENAI_API_VERSION": { + "type": [ + "string", + "null" + ] + }, + "GOOGLE_HOST": { + "type": [ + "string", + "null" + ] + }, + "GCP_PROJECT_ID": { + "type": [ + "string", + "null" + ] + }, + "GCP_LOCATION": { + "type": [ + "string", + "null" + ] + }, + "GCP_MAX_RETRIES": { + "type": [ + "string", + "null" + ] + }, + "GCP_INITIAL_RETRY_INTERVAL_MS": { + "type": [ + "string", + "null" + ] + }, + "GCP_BACKOFF_MULTIPLIER": { + "type": [ + "string", + "null" + ] + }, + "GCP_MAX_RETRY_INTERVAL_MS": { + "type": [ + "string", + "null" + ] + }, + "AWS_REGION": { + "type": [ + "string", + "null" + ] + }, + "AWS_PROFILE": { + "type": [ + "string", + "null" + ] + }, + "BEDROCK_MAX_RETRIES": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "BEDROCK_INITIAL_RETRY_INTERVAL_MS": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "BEDROCK_BACKOFF_MULTIPLIER": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "BEDROCK_MAX_RETRY_INTERVAL_MS": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "BEDROCK_ENABLE_CACHING": { + "type": [ + "boolean", + "null" + ] + }, + "SAGEMAKER_ENDPOINT_NAME": { + "type": [ + "string", + "null" + ] + }, + "LITELLM_HOST": { + "type": [ + "string", + "null" + ] + }, + "LITELLM_BASE_PATH": { + "type": [ + "string", + "null" + ] + }, + "LITELLM_TIMEOUT": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "SNOWFLAKE_HOST": { + "type": [ + "string", + "null" + ] + }, + "GITHUB_COPILOT_HOST": { + "type": [ + "string", + "null" + ] + }, + "GITHUB_COPILOT_CLIENT_ID": { + "type": [ + "string", + "null" + ] + }, + "GITHUB_COPILOT_TOKEN_URL": { + "type": [ + "string", + "null" + ] + }, + "XAI_HOST": { + "type": [ + "string", + "null" + ] + }, + "OPENROUTER_HOST": { + "type": [ + "string", + "null" + ] + }, + "VENICE_HOST": { + "type": [ + "string", + "null" + ] + }, + "VENICE_BASE_PATH": { + "type": [ + "string", + "null" + ] + }, + "VENICE_MODELS_PATH": { + "type": [ + "string", + "null" + ] + }, + "TETRATE_HOST": { + "type": [ + "string", + "null" + ] + }, + "AVIAN_HOST": { + "type": [ + "string", + "null" + ] + }, + "otel_exporter_otlp_endpoint": { + "type": [ + "string", + "null" + ] + }, + "otel_exporter_otlp_timeout": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "tunnel_auto_start": { + "type": [ + "boolean", + "null" + ] + }, + "extensions": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/$defs/ExtensionEntry" + } + }, + "slash_commands": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/$defs/SlashCommandMapping" + } + }, + "experiments": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "boolean" + } + } + }, + "$defs": { + "GooseMode": { + "type": "string", + "enum": [ + "auto", + "approve", + "smart_approve", + "chat" + ] + }, + "ExtensionEntry": { + "description": "Represents the different types of MCP extensions that can be added to the manager", + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + } + }, + "oneOf": [ + { + "description": "SSE transport is no longer supported - kept only for config file compatibility", + "type": "object", + "properties": { + "name": { + "type": "string", + "default": "" + }, + "description": { + "type": "string", + "default": "" + }, + "uri": { + "type": [ + "string", + "null" + ], + "default": null + }, + "type": { + "type": "string", + "const": "sse" + } + }, + "required": [ + "type" + ] + }, + { + "description": "Standard I/O client with command and arguments", + "type": "object", + "properties": { + "name": { + "description": "The name used to identify this extension", + "type": "string" + }, + "description": { + "type": "string", + "default": "" + }, + "cmd": { + "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + }, + "envs": { + "$ref": "#/$defs/Envs", + "default": {} + }, + "env_keys": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "timeout": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "bundled": { + "type": [ + "boolean", + "null" + ], + "default": null + }, + "available_tools": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "type": { + "type": "string", + "const": "stdio" + } + }, + "required": [ + "type", + "name", + "cmd", + "args" + ] + }, + { + "description": "Built-in extension that is part of the bundled goose MCP server", + "type": "object", + "properties": { + "name": { + "description": "The name used to identify this extension", + "type": "string" + }, + "description": { + "type": "string", + "default": "" + }, + "display_name": { + "type": [ + "string", + "null" + ] + }, + "timeout": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "bundled": { + "type": [ + "boolean", + "null" + ], + "default": null + }, + "available_tools": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "type": { + "type": "string", + "const": "builtin" + } + }, + "required": [ + "type", + "name" + ] + }, + { + "description": "Platform extensions that have direct access to the agent etc and run in the agent process", + "type": "object", + "properties": { + "name": { + "description": "The name used to identify this extension", + "type": "string" + }, + "description": { + "type": "string", + "default": "" + }, + "display_name": { + "type": [ + "string", + "null" + ] + }, + "bundled": { + "type": [ + "boolean", + "null" + ], + "default": null + }, + "available_tools": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "type": { + "type": "string", + "const": "platform" + } + }, + "required": [ + "type", + "name" + ] + }, + { + "description": "Streamable HTTP client with a URI endpoint using MCP Streamable HTTP specification", + "type": "object", + "properties": { + "name": { + "description": "The name used to identify this extension", + "type": "string" + }, + "description": { + "type": "string", + "default": "" + }, + "uri": { + "type": "string" + }, + "envs": { + "$ref": "#/$defs/Envs", + "default": {} + }, + "env_keys": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "default": {} + }, + "timeout": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "socket": { + "description": "Optional Unix domain socket path for HTTP-over-UDS transport.\nWhen set, the HTTP connection is routed through this socket while\n`uri` is used for the Host header and path.\nUse `@name` for Linux abstract sockets.", + "type": [ + "string", + "null" + ], + "default": null + }, + "bundled": { + "type": [ + "boolean", + "null" + ], + "default": null + }, + "available_tools": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "type": { + "type": "string", + "const": "streamable_http" + } + }, + "required": [ + "type", + "name", + "uri" + ] + }, + { + "description": "Frontend-provided tools that will be called through the frontend", + "type": "object", + "properties": { + "name": { + "description": "The name used to identify this extension", + "type": "string" + }, + "description": { + "type": "string", + "default": "" + }, + "tools": { + "description": "The tools provided by the frontend", + "type": "array", + "items": { + "$ref": "#/$defs/Tool" + } + }, + "instructions": { + "description": "Instructions for how to use these tools", + "type": [ + "string", + "null" + ] + }, + "bundled": { + "type": [ + "boolean", + "null" + ], + "default": null + }, + "available_tools": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "type": { + "type": "string", + "const": "frontend" + } + }, + "required": [ + "type", + "name", + "tools" + ] + }, + { + "description": "Inline Python code that will be executed using uvx", + "type": "object", + "properties": { + "name": { + "description": "The name used to identify this extension", + "type": "string" + }, + "description": { + "type": "string", + "default": "" + }, + "code": { + "description": "The Python code to execute", + "type": "string" + }, + "timeout": { + "description": "Timeout in seconds", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "dependencies": { + "description": "Python package dependencies required by this extension", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + }, + "default": null + }, + "available_tools": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "type": { + "type": "string", + "const": "inline_python" + } + }, + "required": [ + "type", + "name", + "code" + ] + } + ] + }, + "Envs": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Tool": { + "description": "A tool that can be used by a model.", + "type": "object", + "properties": { + "name": { + "description": "The name of the tool", + "type": "string" + }, + "title": { + "description": "A human-readable title for the tool", + "type": [ + "string", + "null" + ] + }, + "description": { + "description": "A description of what the tool does", + "type": [ + "string", + "null" + ] + }, + "inputSchema": { + "description": "A JSON Schema object defining the expected parameters for the tool", + "type": "object", + "additionalProperties": true + }, + "outputSchema": { + "description": "An optional JSON Schema object defining the structure of the tool's output", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "annotations": { + "description": "Optional additional tool information.", + "anyOf": [ + { + "$ref": "#/$defs/ToolAnnotations" + }, + { + "type": "null" + } + ] + }, + "execution": { + "description": "Execution-related configuration including task support mode.", + "anyOf": [ + { + "$ref": "#/$defs/ToolExecution" + }, + { + "type": "null" + } + ] + }, + "icons": { + "description": "Optional list of icons for the tool", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/$defs/Icon" + } + }, + "_meta": { + "description": "Optional additional metadata for this tool", + "type": [ + "object", + "null" + ], + "additionalProperties": true + } + }, + "required": [ + "name", + "inputSchema" + ] + }, + "ToolAnnotations": { + "description": "Additional properties describing a Tool to clients.\n\nNOTE: all properties in ToolAnnotations are **hints**.\nThey are not guaranteed to provide a faithful description of\ntool behavior (including descriptive properties like `title`).\n\nClients should never make tool use decisions based on ToolAnnotations\nreceived from untrusted servers.", + "type": "object", + "properties": { + "title": { + "description": "A human-readable title for the tool.", + "type": [ + "string", + "null" + ] + }, + "readOnlyHint": { + "description": "If true, the tool does not modify its environment.\n\nDefault: false", + "type": [ + "boolean", + "null" + ] + }, + "destructiveHint": { + "description": "If true, the tool may perform destructive updates to its environment.\nIf false, the tool performs only additive updates.\n\n(This property is meaningful only when `readOnlyHint == false`)\n\nDefault: true\nA human-readable description of the tool's purpose.", + "type": [ + "boolean", + "null" + ] + }, + "idempotentHint": { + "description": "If true, calling the tool repeatedly with the same arguments\nwill have no additional effect on the its environment.\n\n(This property is meaningful only when `readOnlyHint == false`)\n\nDefault: false.", + "type": [ + "boolean", + "null" + ] + }, + "openWorldHint": { + "description": "If true, this tool may interact with an \"open world\" of external\nentities. If false, the tool's domain of interaction is closed.\nFor example, the world of a web search tool is open, whereas that\nof a memory tool is not.\n\nDefault: true", + "type": [ + "boolean", + "null" + ] + } + } + }, + "ToolExecution": { + "description": "Execution-related configuration for a tool.\n\nThis struct contains settings that control how a tool should be executed,\nincluding task support configuration.", + "type": "object", + "properties": { + "taskSupport": { + "description": "Indicates whether this tool supports task-based invocation.\n\nWhen not present or set to `Forbidden`, clients MUST NOT invoke this tool as a task.\nWhen set to `Optional`, clients MAY invoke this tool as a task or normal call.\nWhen set to `Required`, clients MUST invoke this tool as a task.", + "anyOf": [ + { + "$ref": "#/$defs/TaskSupport" + }, + { + "type": "null" + } + ] + } + } + }, + "TaskSupport": { + "description": "Per-tool task support mode as defined in the MCP specification.\n\nThis enum indicates whether a tool supports task-based invocation,\nallowing clients to know how to properly call the tool.\n\nSee [Tool-Level Negotiation](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#tool-level-negotiation).", + "oneOf": [ + { + "description": "Clients MUST NOT invoke this tool as a task (default behavior).", + "type": "string", + "const": "forbidden" + }, + { + "description": "Clients MAY invoke this tool as either a task or a normal call.", + "type": "string", + "const": "optional" + }, + { + "description": "Clients MUST invoke this tool as a task.", + "type": "string", + "const": "required" + } + ] + }, + "Icon": { + "description": "A URL pointing to an icon resource or a base64-encoded data URI.\n\nClients that support rendering icons MUST support at least the following MIME types:\n- image/png - PNG images (safe, universal compatibility)\n- image/jpeg (and image/jpg) - JPEG images (safe, universal compatibility)\n\nClients that support rendering icons SHOULD also support:\n- image/svg+xml - SVG images (scalable but requires security precautions)\n- image/webp - WebP images (modern, efficient format)", + "type": "object", + "properties": { + "src": { + "description": "A standard URI pointing to an icon resource", + "type": "string" + }, + "mimeType": { + "description": "Optional override if the server's MIME type is missing or generic", + "type": [ + "string", + "null" + ] + }, + "sizes": { + "description": "Size specification, each string should be in WxH format (e.g., `\\\"48x48\\\"`, `\\\"96x96\\\"`) or `\\\"any\\\"` for scalable formats like SVG", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "theme": { + "description": "Optional specifier for the theme this icon is designed for\nIf not provided, the client should assume the icon can be used with any theme.", + "anyOf": [ + { + "$ref": "#/$defs/IconTheme" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "src" + ] + }, + "IconTheme": { + "description": "Icon themes supported by the MCP specification", + "oneOf": [ + { + "description": "Indicates the icon is designed to be used with a light background", + "type": "string", + "const": "light" + }, + { + "description": "Indicates the icon is designed to be used with a dark background", + "type": "string", + "const": "dark" + } + ] + }, + "SlashCommandMapping": { + "type": "object", + "properties": { + "command": { + "type": "string" + }, + "recipe_path": { + "type": "string" + } + }, + "required": [ + "command", + "recipe_path" + ] + } + } +} diff --git a/crates/goose/src/acp/server/config.rs b/crates/goose/src/acp/server/config.rs index 2dacf9ece297..40b105501bce 100644 --- a/crates/goose/src/acp/server/config.rs +++ b/crates/goose/src/acp/server/config.rs @@ -111,14 +111,11 @@ impl GooseAcpAgent { let config = self.config()?; config - .set_param_values(&[( - "GOOSE_PROVIDER".to_string(), - serde_json::Value::String(provider_id.clone()), - )]) + .set_goose_provider(provider_id.clone()) .internal_err_ctx("Failed to save default provider")?; if let Some(model_id) = model_id.as_deref() { config - .set_param("GOOSE_MODEL", model_id) + .set_goose_model(model_id.to_string()) .internal_err_ctx("Failed to save default model")?; } else { config diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index 9a4359535d29..b8bd62216cc0 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -403,8 +403,7 @@ impl Agent { self.tool_inspection_manager.apply_tool_annotations(&tools); } - let tool_call_cut_off = match Config::global().get_param::("GOOSE_TOOL_CALL_CUTOFF") - { + let tool_call_cut_off = match Config::global().get_goose_tool_call_cutoff() { Ok(v) => v, Err(_) => { let context_limit = self @@ -413,7 +412,7 @@ impl Agent { .map(|p| p.get_model_config().context_limit()) .unwrap_or(crate::model::DEFAULT_CONTEXT_LIMIT); let compaction_threshold = Config::global() - .get_param::("GOOSE_AUTO_COMPACT_THRESHOLD") + .get_goose_auto_compact_threshold() .unwrap_or(crate::context_mgmt::DEFAULT_COMPACTION_THRESHOLD); crate::context_mgmt::compute_tool_call_cutoff(context_limit, compaction_threshold) } @@ -1168,7 +1167,7 @@ impl Agent { } else { let config = Config::global(); let threshold = config - .get_param::("GOOSE_AUTO_COMPACT_THRESHOLD") + .get_goose_auto_compact_threshold() .unwrap_or(DEFAULT_COMPACTION_THRESHOLD); let threshold_percentage = (threshold * 100.0) as u32; @@ -1292,7 +1291,7 @@ impl Agent { let mut turns_taken = 0u32; let max_turns = session_config.max_turns.unwrap_or_else(|| { Config::global() - .get_param::("GOOSE_MAX_TURNS") + .get_goose_max_turns() .unwrap_or(DEFAULT_MAX_TURNS) }); let mut compaction_attempts = 0; diff --git a/crates/goose/src/agents/extension.rs b/crates/goose/src/agents/extension.rs index 51349e0115e1..f8c3d8223ee1 100644 --- a/crates/goose/src/agents/extension.rs +++ b/crates/goose/src/agents/extension.rs @@ -7,6 +7,7 @@ use crate::config::Config; use rmcp::model::Tool; use rmcp::service::ClientInitializeError; use rmcp::ServiceError as ClientError; +use schemars::JsonSchema; use serde::Deserializer; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -58,7 +59,7 @@ pub enum ExtensionError { pub type ExtensionResult = Result; -#[derive(Debug, Clone, Deserialize, Serialize, Default, ToSchema, PartialEq)] +#[derive(Debug, Clone, Deserialize, Serialize, Default, ToSchema, PartialEq, JsonSchema)] pub struct Envs { /// A map of environment variables to set, e.g. API_KEY -> some_secret, HOST -> host #[serde(default)] @@ -148,7 +149,7 @@ impl Envs { } /// Represents the different types of MCP extensions that can be added to the manager -#[derive(Debug, Clone, Deserialize, Serialize, ToSchema, PartialEq)] +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema, PartialEq, JsonSchema)] #[serde(tag = "type")] pub enum ExtensionConfig { /// SSE transport is no longer supported - kept only for config file compatibility diff --git a/crates/goose/src/agents/platform_extensions/code_execution.rs b/crates/goose/src/agents/platform_extensions/code_execution.rs index 6fc50fcbf6c3..42d191ad3f5c 100644 --- a/crates/goose/src/agents/platform_extensions/code_execution.rs +++ b/crates/goose/src/agents/platform_extensions/code_execution.rs @@ -518,7 +518,7 @@ impl McpClientTrait for CodeExecutionClient { pub fn get_tool_disclosure() -> ToolDisclosure { let config = crate::config::Config::global(); let tool_disclosure_str: String = config - .get_param("CODE_MODE_TOOL_DISCLOSURE") + .get_code_mode_tool_disclosure() .unwrap_or_else(|_| "catalog".to_string()); serde_json::from_value(serde_json::json!(tool_disclosure_str)).unwrap_or_default() } diff --git a/crates/goose/src/agents/platform_extensions/summon.rs b/crates/goose/src/agents/platform_extensions/summon.rs index 68eb229e90ee..e7c70f1ae9db 100644 --- a/crates/goose/src/agents/platform_extensions/summon.rs +++ b/crates/goose/src/agents/platform_extensions/summon.rs @@ -306,7 +306,7 @@ fn current_epoch_millis() -> u64 { /// Get maximum number of concurrent background tasks fn max_background_tasks() -> usize { Config::global() - .get_param::("GOOSE_MAX_BACKGROUND_TASKS") + .get_goose_max_background_tasks() .unwrap_or(5) } @@ -1290,11 +1290,7 @@ impl SummonClient { .as_ref() .and_then(|s| s.goose_provider.clone()) }) - .or_else(|| { - Config::global() - .get_param::("GOOSE_SUBAGENT_PROVIDER") - .ok() - }) + .or_else(|| Config::global().get_goose_subagent_provider().ok()) .or_else(|| session.provider_name.clone()) .ok_or_else(|| anyhow::anyhow!("No provider configured"))?; @@ -1311,7 +1307,7 @@ impl SummonClient { .and_then(|s| s.goose_model.as_ref()) { model_config.model_name = model.clone(); - } else if let Ok(model) = Config::global().get_param::("GOOSE_SUBAGENT_MODEL") { + } else if let Ok(model) = Config::global().get_goose_subagent_model() { model_config.model_name = model; } @@ -1335,11 +1331,7 @@ impl SummonClient { .ok() .and_then(|v| v.parse().ok()) }) - .or_else(|| { - Config::global() - .get_param::("GOOSE_SUBAGENT_MAX_TURNS") - .ok() - }) + .or_else(|| Config::global().get_goose_subagent_max_turns().ok()) .unwrap_or(DEFAULT_SUBAGENT_MAX_TURNS) } diff --git a/crates/goose/src/agents/retry.rs b/crates/goose/src/agents/retry.rs index 367c557ef515..5f1df66a5035 100644 --- a/crates/goose/src/agents/retry.rs +++ b/crates/goose/src/agents/retry.rs @@ -30,12 +30,6 @@ pub enum RetryResult { Retried, } -/// Environment variable for configuring retry timeout globally -const GOOSE_RECIPE_RETRY_TIMEOUT_SECONDS: &str = "GOOSE_RECIPE_RETRY_TIMEOUT_SECONDS"; - -/// Environment variable for configuring on_failure timeout globally -const GOOSE_RECIPE_ON_FAILURE_TIMEOUT_SECONDS: &str = "GOOSE_RECIPE_ON_FAILURE_TIMEOUT_SECONDS"; - /// Manages retry state and operations for agent execution #[derive(Debug)] pub struct RetryManager { @@ -169,7 +163,7 @@ fn get_retry_timeout(retry_config: &RetryConfig) -> Duration { .timeout_seconds .or_else(|| { let config = Config::global(); - config.get_param(GOOSE_RECIPE_RETRY_TIMEOUT_SECONDS).ok() + config.get_goose_recipe_retry_timeout_seconds().ok() }) .unwrap_or(DEFAULT_RETRY_TIMEOUT_SECONDS); @@ -183,9 +177,7 @@ fn get_on_failure_timeout(retry_config: &RetryConfig) -> Duration { .on_failure_timeout_seconds .or_else(|| { let config = Config::global(); - config - .get_param(GOOSE_RECIPE_ON_FAILURE_TIMEOUT_SECONDS) - .ok() + config.get_goose_recipe_on_failure_timeout_seconds().ok() }) .unwrap_or(DEFAULT_ON_FAILURE_TIMEOUT_SECONDS); diff --git a/crates/goose/src/agents/subagent_task_config.rs b/crates/goose/src/agents/subagent_task_config.rs index 1cca9c086cbd..774ce6034541 100644 --- a/crates/goose/src/agents/subagent_task_config.rs +++ b/crates/goose/src/agents/subagent_task_config.rs @@ -44,7 +44,7 @@ impl TaskConfig { extensions, max_turns: Some( Config::global() - .get_param::("GOOSE_SUBAGENT_MAX_TURNS") + .get_goose_subagent_max_turns() .unwrap_or(DEFAULT_SUBAGENT_MAX_TURNS), ), } diff --git a/crates/goose/src/bin/generate_config_schema.rs b/crates/goose/src/bin/generate_config_schema.rs new file mode 100644 index 000000000000..d81efa7e6b5f --- /dev/null +++ b/crates/goose/src/bin/generate_config_schema.rs @@ -0,0 +1,13 @@ +use goose::config::schema::GooseConfigSchema; +use schemars::schema_for; +use std::{env, fs, path::PathBuf}; + +fn main() { + let schema = schema_for!(GooseConfigSchema); + let json = serde_json::to_string_pretty(&schema).expect("failed to serialize schema"); + + let dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let path = PathBuf::from(&dir).join("config.schema.json"); + fs::write(&path, format!("{json}\n")).expect("failed to write schema file"); + eprintln!("Generated config schema at {}", path.display()); +} diff --git a/crates/goose/src/config/base.rs b/crates/goose/src/config/base.rs index e9f608ab9904..9472ca607e87 100644 --- a/crates/goose/src/config/base.rs +++ b/crates/goose/src/config/base.rs @@ -204,6 +204,14 @@ pub trait ConfigValue { macro_rules! config_value { ($key:ident, $type:ty) => { + const _: () = assert!( + crate::config::schema::GooseConfigSchema::has_key(stringify!($key)), + concat!( + "Config key ", + stringify!($key), + " is not registered in GooseConfigSchema" + ) + ); impl Config { pastey::paste! { pub fn [](&self) -> Result<$type, ConfigError> { @@ -219,6 +227,14 @@ macro_rules! config_value { }; ($key:ident, $inner:ty, $default:expr) => { + const _: () = assert!( + crate::config::schema::GooseConfigSchema::has_key(stringify!($key)), + concat!( + "Config key ", + stringify!($key), + " is not registered in GooseConfigSchema" + ) + ); pastey::paste! { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(transparent)] @@ -1033,6 +1049,124 @@ config_value!(CLAUDE_THINKING_EFFORT, String); config_value!(CLAUDE_THINKING_BUDGET, i32); config_value!(GOOSE_DEFAULT_EXTENSION_TIMEOUT, u64); +// Core Goose Settings +config_value!(GOOSE_MAX_TOKENS, i32); +config_value!(GOOSE_CONTEXT_LIMIT, usize); +config_value!(GOOSE_INPUT_LIMIT, usize); +config_value!(GOOSE_MAX_TURNS, u32); +config_value!(GOOSE_AUTO_COMPACT_THRESHOLD, f64); +config_value!(GOOSE_TOOL_PAIR_SUMMARIZATION, bool); +config_value!(GOOSE_TOOL_CALL_CUTOFF, usize); +config_value!(GOOSE_STREAM_TIMEOUT, u64); +config_value!(GOOSE_DISABLE_KEYRING, String); +config_value!(GOOSE_TELEMETRY_ENABLED, bool); +config_value!(GOOSE_ALLOWLIST, String); +config_value!(GOOSE_SYSTEM_PROMPT_FILE_PATH, String); +config_value!(GOOSE_DEBUG, bool); +config_value!(GOOSE_SHOW_FULL_OUTPUT, bool); +config_value!(GOOSE_STATUS_HOOK, String); +config_value!(GOOSE_LOCAL_ENABLE_THINKING, bool); +config_value!(GOOSE_DATABRICKS_CLIENT_REQUEST_ID, bool); +config_value!(CONTEXT_FILE_NAMES, Vec); +config_value!(EDIT_MODE, String); +config_value!(RANDOM_THINKING_MESSAGES, bool); +config_value!(CODE_MODE_TOOL_DISCLOSURE, String); + +// mTLS Settings +config_value!(GOOSE_CLIENT_CERT_PATH, String); +config_value!(GOOSE_CLIENT_KEY_PATH, String); +config_value!(GOOSE_CA_CERT_PATH, String); + +// Planner & Subagent Settings +config_value!(GOOSE_PLANNER_PROVIDER, String); +config_value!(GOOSE_PLANNER_MODEL, String); +config_value!(GOOSE_SUBAGENT_PROVIDER, String); +config_value!(GOOSE_SUBAGENT_MODEL, String); +config_value!(GOOSE_SUBAGENT_MAX_TURNS, usize); +config_value!(GOOSE_MAX_BACKGROUND_TASKS, usize); + +// Recipe Settings +config_value!(GOOSE_RECIPE_GITHUB_REPO, Option); +config_value!(GOOSE_RECIPE_RETRY_TIMEOUT_SECONDS, u64); +config_value!(GOOSE_RECIPE_ON_FAILURE_TIMEOUT_SECONDS, u64); + +// CLI Settings +config_value!(GOOSE_CLI_MIN_PRIORITY, f32); +config_value!(GOOSE_CLI_THEME, String); +config_value!(GOOSE_CLI_LIGHT_THEME, String); +config_value!(GOOSE_CLI_DARK_THEME, String); +config_value!(GOOSE_CLI_SHOW_COST, bool); +config_value!(GOOSE_CLI_SHOW_THINKING, bool); +config_value!(GOOSE_CLI_NEWLINE_KEY, String); + +// Security Settings +config_value!(SECURITY_PROMPT_ENABLED, bool); +config_value!(SECURITY_PROMPT_THRESHOLD, f64); +config_value!(SECURITY_PROMPT_CLASSIFIER_ENABLED, bool); +config_value!(SECURITY_PROMPT_CLASSIFIER_MODEL, String); +config_value!(SECURITY_PROMPT_CLASSIFIER_ENDPOINT, String); +config_value!(SECURITY_COMMAND_CLASSIFIER_ENABLED, bool); + +// Provider Settings +config_value!(OPENAI_HOST, String); +config_value!(OPENAI_BASE_URL, String); +config_value!(OPENAI_BASE_PATH, String); +config_value!(OPENAI_ORGANIZATION, String); +config_value!(OPENAI_PROJECT, String); +config_value!(OPENAI_TIMEOUT, u64); +config_value!(ANTHROPIC_HOST, String); +config_value!(OLLAMA_HOST, String); +config_value!(OLLAMA_TIMEOUT, u64); +config_value!(OLLAMA_STREAM_TIMEOUT, u64); +config_value!(OLLAMA_STREAM_USAGE, bool); +config_value!(DATABRICKS_HOST, String); +config_value!(DATABRICKS_MAX_RETRIES, String); +config_value!(DATABRICKS_INITIAL_RETRY_INTERVAL_MS, String); +config_value!(DATABRICKS_BACKOFF_MULTIPLIER, String); +config_value!(DATABRICKS_MAX_RETRY_INTERVAL_MS, String); +config_value!(AZURE_OPENAI_ENDPOINT, String); +config_value!(AZURE_OPENAI_DEPLOYMENT_NAME, String); +config_value!(AZURE_OPENAI_API_VERSION, String); +config_value!(GOOGLE_HOST, String); +config_value!(GCP_PROJECT_ID, String); +config_value!(GCP_LOCATION, String); +config_value!(GCP_MAX_RETRIES, String); +config_value!(GCP_INITIAL_RETRY_INTERVAL_MS, String); +config_value!(GCP_BACKOFF_MULTIPLIER, String); +config_value!(GCP_MAX_RETRY_INTERVAL_MS, String); +config_value!(AWS_REGION, String); +config_value!(AWS_PROFILE, String); +config_value!(BEDROCK_MAX_RETRIES, usize); +config_value!(BEDROCK_INITIAL_RETRY_INTERVAL_MS, u64); +config_value!(BEDROCK_BACKOFF_MULTIPLIER, f64); +config_value!(BEDROCK_MAX_RETRY_INTERVAL_MS, u64); +config_value!(BEDROCK_ENABLE_CACHING, bool); +config_value!(SAGEMAKER_ENDPOINT_NAME, String); +config_value!(LITELLM_HOST, String); +config_value!(LITELLM_BASE_PATH, String); +config_value!(LITELLM_TIMEOUT, u64); +config_value!(SNOWFLAKE_HOST, String); +config_value!(GITHUB_COPILOT_HOST, String); +config_value!(GITHUB_COPILOT_CLIENT_ID, String); +config_value!(GITHUB_COPILOT_TOKEN_URL, String); +config_value!(XAI_HOST, String); +config_value!(OPENROUTER_HOST, String); +config_value!(VENICE_HOST, String); +config_value!(VENICE_BASE_PATH, String); +config_value!(VENICE_MODELS_PATH, String); +config_value!(TETRATE_HOST, String); +config_value!(AVIAN_HOST, String); + +// Observability Settings +config_value!(otel_exporter_otlp_endpoint, String); +config_value!(otel_exporter_otlp_timeout, u64); + +// Tunnel Settings +config_value!(tunnel_auto_start, bool); + +// Thinking Settings +config_value!(GEMINI25_THINKING_BUDGET, i32); + #[cfg(test)] mod tests { use super::*; diff --git a/crates/goose/src/config/extensions.rs b/crates/goose/src/config/extensions.rs index 3b47f2859603..39c8b4af1354 100644 --- a/crates/goose/src/config/extensions.rs +++ b/crates/goose/src/config/extensions.rs @@ -2,6 +2,7 @@ use super::base::Config; use crate::agents::extension::PLATFORM_EXTENSIONS; use crate::agents::ExtensionConfig; use indexmap::IndexMap; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_yaml::Mapping; use tracing::warn; @@ -17,7 +18,7 @@ fn default_extension_enabled() -> bool { true } -#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] +#[derive(Debug, Deserialize, Serialize, Clone, ToSchema, JsonSchema)] pub struct ExtensionEntry { #[serde(default = "default_extension_enabled")] pub enabled: bool, diff --git a/crates/goose/src/config/goose_mode.rs b/crates/goose/src/config/goose_mode.rs index dbfe8af1282f..65df7693fd59 100644 --- a/crates/goose/src/config/goose_mode.rs +++ b/crates/goose/src/config/goose_mode.rs @@ -1,3 +1,4 @@ +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use strum::{Display, EnumMessage, EnumString, IntoStaticStr, VariantNames}; use utoipa::ToSchema; @@ -18,6 +19,7 @@ use utoipa::ToSchema; IntoStaticStr, VariantNames, ToSchema, + JsonSchema, )] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] diff --git a/crates/goose/src/config/mod.rs b/crates/goose/src/config/mod.rs index cd731c2ae3e2..463c06eab6bb 100644 --- a/crates/goose/src/config/mod.rs +++ b/crates/goose/src/config/mod.rs @@ -6,6 +6,7 @@ pub mod goose_mode; mod migrations; pub mod paths; pub mod permission; +pub mod schema; pub mod search_path; pub mod signup_nanogpt; pub mod signup_openrouter; diff --git a/crates/goose/src/config/schema.rs b/crates/goose/src/config/schema.rs new file mode 100644 index 000000000000..5ea7465b839d --- /dev/null +++ b/crates/goose/src/config/schema.rs @@ -0,0 +1,476 @@ +use std::collections::HashMap; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::config::extensions::ExtensionEntry; +use crate::config::goose_mode::GooseMode; +use crate::slash_commands::SlashCommandMapping; + +/// JSON Schema representation of Goose's config.yaml. +/// +/// All keys are optional. Unknown keys are allowed (additionalProperties: true) +/// because Goose passes undocumented provider-specific keys through as +/// environment variable overrides. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct GooseConfigSchema { + // === Core Goose Settings === + #[serde(rename = "GOOSE_PROVIDER")] + pub goose_provider: Option, + #[serde(rename = "GOOSE_MODEL")] + pub goose_model: Option, + #[serde(rename = "GOOSE_MODE")] + pub goose_mode: Option, + #[serde(rename = "GOOSE_MAX_TOKENS")] + pub goose_max_tokens: Option, + #[serde(rename = "GOOSE_CONTEXT_LIMIT")] + pub goose_context_limit: Option, + #[serde(rename = "GOOSE_INPUT_LIMIT")] + pub goose_input_limit: Option, + #[serde(rename = "GOOSE_MAX_TURNS")] + pub goose_max_turns: Option, + #[serde(rename = "GOOSE_MAX_ACTIVE_AGENTS")] + pub goose_max_active_agents: Option, + #[serde(rename = "GOOSE_AUTO_COMPACT_THRESHOLD")] + pub goose_auto_compact_threshold: Option, + #[serde(rename = "GOOSE_TOOL_PAIR_SUMMARIZATION")] + pub goose_tool_pair_summarization: Option, + #[serde(rename = "GOOSE_TOOL_CALL_CUTOFF")] + pub goose_tool_call_cutoff: Option, + #[serde(rename = "GOOSE_STREAM_TIMEOUT")] + pub goose_stream_timeout: Option, + #[serde(rename = "GOOSE_SEARCH_PATHS")] + pub goose_search_paths: Option>, + #[serde(rename = "GOOSE_DISABLE_SESSION_NAMING")] + pub goose_disable_session_naming: Option, + #[serde(rename = "GOOSE_DISABLE_KEYRING")] + pub goose_disable_keyring: Option, + #[serde(rename = "GOOSE_TELEMETRY_ENABLED")] + pub goose_telemetry_enabled: Option, + #[serde(rename = "GOOSE_DEFAULT_EXTENSION_TIMEOUT")] + pub goose_default_extension_timeout: Option, + #[serde(rename = "GOOSE_PROMPT_EDITOR")] + pub goose_prompt_editor: Option, + #[serde(rename = "GOOSE_PROMPT_EDITOR_ALWAYS")] + pub goose_prompt_editor_always: Option, + #[serde(rename = "GOOSE_ALLOWLIST")] + pub goose_allowlist: Option, + #[serde(rename = "GOOSE_SYSTEM_PROMPT_FILE_PATH")] + pub goose_system_prompt_file_path: Option, + #[serde(rename = "GOOSE_DEBUG")] + pub goose_debug: Option, + #[serde(rename = "GOOSE_SHOW_FULL_OUTPUT")] + pub goose_show_full_output: Option, + #[serde(rename = "GOOSE_STATUS_HOOK")] + pub goose_status_hook: Option, + #[serde(rename = "GOOSE_LOCAL_ENABLE_THINKING")] + pub goose_local_enable_thinking: Option, + #[serde(rename = "GOOSE_DATABRICKS_CLIENT_REQUEST_ID")] + pub goose_databricks_client_request_id: Option, + #[serde(rename = "CONTEXT_FILE_NAMES")] + pub context_file_names: Option>, + #[serde(rename = "EDIT_MODE")] + pub edit_mode: Option, + #[serde(rename = "RANDOM_THINKING_MESSAGES")] + pub random_thinking_messages: Option, + #[serde(rename = "CODE_MODE_TOOL_DISCLOSURE")] + pub code_mode_tool_disclosure: Option, + + // === mTLS Settings === + #[serde(rename = "GOOSE_CLIENT_CERT_PATH")] + pub goose_client_cert_path: Option, + #[serde(rename = "GOOSE_CLIENT_KEY_PATH")] + pub goose_client_key_path: Option, + #[serde(rename = "GOOSE_CA_CERT_PATH")] + pub goose_ca_cert_path: Option, + + // === Planner & Subagent Settings === + #[serde(rename = "GOOSE_PLANNER_PROVIDER")] + pub goose_planner_provider: Option, + #[serde(rename = "GOOSE_PLANNER_MODEL")] + pub goose_planner_model: Option, + #[serde(rename = "GOOSE_SUBAGENT_PROVIDER")] + pub goose_subagent_provider: Option, + #[serde(rename = "GOOSE_SUBAGENT_MODEL")] + pub goose_subagent_model: Option, + #[serde(rename = "GOOSE_SUBAGENT_MAX_TURNS")] + pub goose_subagent_max_turns: Option, + #[serde(rename = "GOOSE_MAX_BACKGROUND_TASKS")] + pub goose_max_background_tasks: Option, + + // === Recipe Settings === + #[serde(rename = "GOOSE_RECIPE_GITHUB_REPO")] + pub goose_recipe_github_repo: Option, + #[serde(rename = "GOOSE_RECIPE_RETRY_TIMEOUT_SECONDS")] + pub goose_recipe_retry_timeout_seconds: Option, + #[serde(rename = "GOOSE_RECIPE_ON_FAILURE_TIMEOUT_SECONDS")] + pub goose_recipe_on_failure_timeout_seconds: Option, + + // === CLI Settings === + #[serde(rename = "GOOSE_CLI_MIN_PRIORITY")] + pub goose_cli_min_priority: Option, + #[serde(rename = "GOOSE_CLI_THEME")] + pub goose_cli_theme: Option, + #[serde(rename = "GOOSE_CLI_LIGHT_THEME")] + pub goose_cli_light_theme: Option, + #[serde(rename = "GOOSE_CLI_DARK_THEME")] + pub goose_cli_dark_theme: Option, + #[serde(rename = "GOOSE_CLI_SHOW_COST")] + pub goose_cli_show_cost: Option, + #[serde(rename = "GOOSE_CLI_SHOW_THINKING")] + pub goose_cli_show_thinking: Option, + #[serde(rename = "GOOSE_CLI_NEWLINE_KEY")] + pub goose_cli_newline_key: Option, + + // === AI Agent / Thinking Settings === + #[serde(rename = "CLAUDE_CODE_COMMAND")] + pub claude_code_command: Option, + #[serde(rename = "GEMINI_CLI_COMMAND")] + pub gemini_cli_command: Option, + #[serde(rename = "CURSOR_AGENT_COMMAND")] + pub cursor_agent_command: Option, + #[serde(rename = "CODEX_COMMAND")] + pub codex_command: Option, + #[serde(rename = "CODEX_REASONING_EFFORT")] + pub codex_reasoning_effort: Option, + #[serde(rename = "CODEX_ENABLE_SKILLS")] + pub codex_enable_skills: Option, + #[serde(rename = "CODEX_SKIP_GIT_CHECK")] + pub codex_skip_git_check: Option, + #[serde(rename = "CHATGPT_CODEX_REASONING_EFFORT")] + pub chatgpt_codex_reasoning_effort: Option, + #[serde(rename = "CLAUDE_THINKING_TYPE")] + pub claude_thinking_type: Option, + #[serde(rename = "CLAUDE_THINKING_EFFORT")] + pub claude_thinking_effort: Option, + #[serde(rename = "CLAUDE_THINKING_BUDGET")] + pub claude_thinking_budget: Option, + #[serde(rename = "GEMINI3_THINKING_LEVEL")] + pub gemini3_thinking_level: Option, + #[serde(rename = "GEMINI25_THINKING_BUDGET")] + pub gemini25_thinking_budget: Option, + + // === Security Settings === + #[serde(rename = "SECURITY_PROMPT_ENABLED")] + pub security_prompt_enabled: Option, + #[serde(rename = "SECURITY_PROMPT_THRESHOLD")] + pub security_prompt_threshold: Option, + #[serde(rename = "SECURITY_PROMPT_CLASSIFIER_ENABLED")] + pub security_prompt_classifier_enabled: Option, + #[serde(rename = "SECURITY_PROMPT_CLASSIFIER_MODEL")] + pub security_prompt_classifier_model: Option, + #[serde(rename = "SECURITY_PROMPT_CLASSIFIER_ENDPOINT")] + pub security_prompt_classifier_endpoint: Option, + #[serde(rename = "SECURITY_COMMAND_CLASSIFIER_ENABLED")] + pub security_command_classifier_enabled: Option, + + // === Provider Settings === + #[serde(rename = "OPENAI_HOST")] + pub openai_host: Option, + #[serde(rename = "OPENAI_BASE_URL")] + pub openai_base_url: Option, + #[serde(rename = "OPENAI_BASE_PATH")] + pub openai_base_path: Option, + #[serde(rename = "OPENAI_ORGANIZATION")] + pub openai_organization: Option, + #[serde(rename = "OPENAI_PROJECT")] + pub openai_project: Option, + #[serde(rename = "OPENAI_TIMEOUT")] + pub openai_timeout: Option, + #[serde(rename = "ANTHROPIC_HOST")] + pub anthropic_host: Option, + #[serde(rename = "OLLAMA_HOST")] + pub ollama_host: Option, + #[serde(rename = "OLLAMA_TIMEOUT")] + pub ollama_timeout: Option, + #[serde(rename = "OLLAMA_STREAM_TIMEOUT")] + pub ollama_stream_timeout: Option, + #[serde(rename = "OLLAMA_STREAM_USAGE")] + pub ollama_stream_usage: Option, + #[serde(rename = "DATABRICKS_HOST")] + pub databricks_host: Option, + #[serde(rename = "DATABRICKS_MAX_RETRIES")] + pub databricks_max_retries: Option, + #[serde(rename = "DATABRICKS_INITIAL_RETRY_INTERVAL_MS")] + pub databricks_initial_retry_interval_ms: Option, + #[serde(rename = "DATABRICKS_BACKOFF_MULTIPLIER")] + pub databricks_backoff_multiplier: Option, + #[serde(rename = "DATABRICKS_MAX_RETRY_INTERVAL_MS")] + pub databricks_max_retry_interval_ms: Option, + #[serde(rename = "AZURE_OPENAI_ENDPOINT")] + pub azure_openai_endpoint: Option, + #[serde(rename = "AZURE_OPENAI_DEPLOYMENT_NAME")] + pub azure_openai_deployment_name: Option, + #[serde(rename = "AZURE_OPENAI_API_VERSION")] + pub azure_openai_api_version: Option, + #[serde(rename = "GOOGLE_HOST")] + pub google_host: Option, + #[serde(rename = "GCP_PROJECT_ID")] + pub gcp_project_id: Option, + #[serde(rename = "GCP_LOCATION")] + pub gcp_location: Option, + #[serde(rename = "GCP_MAX_RETRIES")] + pub gcp_max_retries: Option, + #[serde(rename = "GCP_INITIAL_RETRY_INTERVAL_MS")] + pub gcp_initial_retry_interval_ms: Option, + #[serde(rename = "GCP_BACKOFF_MULTIPLIER")] + pub gcp_backoff_multiplier: Option, + #[serde(rename = "GCP_MAX_RETRY_INTERVAL_MS")] + pub gcp_max_retry_interval_ms: Option, + #[serde(rename = "AWS_REGION")] + pub aws_region: Option, + #[serde(rename = "AWS_PROFILE")] + pub aws_profile: Option, + #[serde(rename = "BEDROCK_MAX_RETRIES")] + pub bedrock_max_retries: Option, + #[serde(rename = "BEDROCK_INITIAL_RETRY_INTERVAL_MS")] + pub bedrock_initial_retry_interval_ms: Option, + #[serde(rename = "BEDROCK_BACKOFF_MULTIPLIER")] + pub bedrock_backoff_multiplier: Option, + #[serde(rename = "BEDROCK_MAX_RETRY_INTERVAL_MS")] + pub bedrock_max_retry_interval_ms: Option, + #[serde(rename = "BEDROCK_ENABLE_CACHING")] + pub bedrock_enable_caching: Option, + #[serde(rename = "SAGEMAKER_ENDPOINT_NAME")] + pub sagemaker_endpoint_name: Option, + #[serde(rename = "LITELLM_HOST")] + pub litellm_host: Option, + #[serde(rename = "LITELLM_BASE_PATH")] + pub litellm_base_path: Option, + #[serde(rename = "LITELLM_TIMEOUT")] + pub litellm_timeout: Option, + #[serde(rename = "SNOWFLAKE_HOST")] + pub snowflake_host: Option, + #[serde(rename = "GITHUB_COPILOT_HOST")] + pub github_copilot_host: Option, + #[serde(rename = "GITHUB_COPILOT_CLIENT_ID")] + pub github_copilot_client_id: Option, + #[serde(rename = "GITHUB_COPILOT_TOKEN_URL")] + pub github_copilot_token_url: Option, + #[serde(rename = "XAI_HOST")] + pub xai_host: Option, + #[serde(rename = "OPENROUTER_HOST")] + pub openrouter_host: Option, + #[serde(rename = "VENICE_HOST")] + pub venice_host: Option, + #[serde(rename = "VENICE_BASE_PATH")] + pub venice_base_path: Option, + #[serde(rename = "VENICE_MODELS_PATH")] + pub venice_models_path: Option, + #[serde(rename = "TETRATE_HOST")] + pub tetrate_host: Option, + #[serde(rename = "AVIAN_HOST")] + pub avian_host: Option, + + // === Observability Settings (lowercase keys) === + pub otel_exporter_otlp_endpoint: Option, + pub otel_exporter_otlp_timeout: Option, + + // === Tunnel Settings (lowercase keys) === + pub tunnel_auto_start: Option, + + // === Structured Config (lowercase keys) === + pub extensions: Option>, + pub slash_commands: Option>, + pub experiments: Option>, +} + +impl GooseConfigSchema { + /// All user-facing config keys that get `config_value!` typed accessors. + /// Category B keys (extensions, slash_commands, experiments) are in the struct + /// for schema generation but NOT here — they use dedicated module helpers. + pub const ALL_KEYS: &[&str] = &[ + // Core Goose Settings + "GOOSE_PROVIDER", + "GOOSE_MODEL", + "GOOSE_MODE", + "GOOSE_MAX_TOKENS", + "GOOSE_CONTEXT_LIMIT", + "GOOSE_INPUT_LIMIT", + "GOOSE_MAX_TURNS", + "GOOSE_MAX_ACTIVE_AGENTS", + "GOOSE_AUTO_COMPACT_THRESHOLD", + "GOOSE_TOOL_PAIR_SUMMARIZATION", + "GOOSE_TOOL_CALL_CUTOFF", + "GOOSE_STREAM_TIMEOUT", + "GOOSE_SEARCH_PATHS", + "GOOSE_DISABLE_SESSION_NAMING", + "GOOSE_DISABLE_KEYRING", + "GOOSE_TELEMETRY_ENABLED", + "GOOSE_DEFAULT_EXTENSION_TIMEOUT", + "GOOSE_PROMPT_EDITOR", + "GOOSE_PROMPT_EDITOR_ALWAYS", + "GOOSE_ALLOWLIST", + "GOOSE_SYSTEM_PROMPT_FILE_PATH", + "GOOSE_DEBUG", + "GOOSE_SHOW_FULL_OUTPUT", + "GOOSE_STATUS_HOOK", + "GOOSE_LOCAL_ENABLE_THINKING", + "GOOSE_DATABRICKS_CLIENT_REQUEST_ID", + "CONTEXT_FILE_NAMES", + "EDIT_MODE", + "RANDOM_THINKING_MESSAGES", + "CODE_MODE_TOOL_DISCLOSURE", + // mTLS Settings + "GOOSE_CLIENT_CERT_PATH", + "GOOSE_CLIENT_KEY_PATH", + "GOOSE_CA_CERT_PATH", + // Planner & Subagent Settings + "GOOSE_PLANNER_PROVIDER", + "GOOSE_PLANNER_MODEL", + "GOOSE_SUBAGENT_PROVIDER", + "GOOSE_SUBAGENT_MODEL", + "GOOSE_SUBAGENT_MAX_TURNS", + "GOOSE_MAX_BACKGROUND_TASKS", + // Recipe Settings + "GOOSE_RECIPE_GITHUB_REPO", + "GOOSE_RECIPE_RETRY_TIMEOUT_SECONDS", + "GOOSE_RECIPE_ON_FAILURE_TIMEOUT_SECONDS", + // CLI Settings + "GOOSE_CLI_MIN_PRIORITY", + "GOOSE_CLI_THEME", + "GOOSE_CLI_LIGHT_THEME", + "GOOSE_CLI_DARK_THEME", + "GOOSE_CLI_SHOW_COST", + "GOOSE_CLI_SHOW_THINKING", + "GOOSE_CLI_NEWLINE_KEY", + // AI Agent / Thinking Settings + "CLAUDE_CODE_COMMAND", + "GEMINI_CLI_COMMAND", + "CURSOR_AGENT_COMMAND", + "CODEX_COMMAND", + "CODEX_REASONING_EFFORT", + "CODEX_ENABLE_SKILLS", + "CODEX_SKIP_GIT_CHECK", + "CHATGPT_CODEX_REASONING_EFFORT", + "CLAUDE_THINKING_TYPE", + "CLAUDE_THINKING_EFFORT", + "CLAUDE_THINKING_BUDGET", + "GEMINI3_THINKING_LEVEL", + "GEMINI25_THINKING_BUDGET", + // Security Settings + "SECURITY_PROMPT_ENABLED", + "SECURITY_PROMPT_THRESHOLD", + "SECURITY_PROMPT_CLASSIFIER_ENABLED", + "SECURITY_PROMPT_CLASSIFIER_MODEL", + "SECURITY_PROMPT_CLASSIFIER_ENDPOINT", + "SECURITY_COMMAND_CLASSIFIER_ENABLED", + // Provider Settings + "OPENAI_HOST", + "OPENAI_BASE_URL", + "OPENAI_BASE_PATH", + "OPENAI_ORGANIZATION", + "OPENAI_PROJECT", + "OPENAI_TIMEOUT", + "ANTHROPIC_HOST", + "OLLAMA_HOST", + "OLLAMA_TIMEOUT", + "OLLAMA_STREAM_TIMEOUT", + "OLLAMA_STREAM_USAGE", + "DATABRICKS_HOST", + "DATABRICKS_MAX_RETRIES", + "DATABRICKS_INITIAL_RETRY_INTERVAL_MS", + "DATABRICKS_BACKOFF_MULTIPLIER", + "DATABRICKS_MAX_RETRY_INTERVAL_MS", + "AZURE_OPENAI_ENDPOINT", + "AZURE_OPENAI_DEPLOYMENT_NAME", + "AZURE_OPENAI_API_VERSION", + "GOOGLE_HOST", + "GCP_PROJECT_ID", + "GCP_LOCATION", + "GCP_MAX_RETRIES", + "GCP_INITIAL_RETRY_INTERVAL_MS", + "GCP_BACKOFF_MULTIPLIER", + "GCP_MAX_RETRY_INTERVAL_MS", + "AWS_REGION", + "AWS_PROFILE", + "BEDROCK_MAX_RETRIES", + "BEDROCK_INITIAL_RETRY_INTERVAL_MS", + "BEDROCK_BACKOFF_MULTIPLIER", + "BEDROCK_MAX_RETRY_INTERVAL_MS", + "BEDROCK_ENABLE_CACHING", + "SAGEMAKER_ENDPOINT_NAME", + "LITELLM_HOST", + "LITELLM_BASE_PATH", + "LITELLM_TIMEOUT", + "SNOWFLAKE_HOST", + "GITHUB_COPILOT_HOST", + "GITHUB_COPILOT_CLIENT_ID", + "GITHUB_COPILOT_TOKEN_URL", + "XAI_HOST", + "OPENROUTER_HOST", + "VENICE_HOST", + "VENICE_BASE_PATH", + "VENICE_MODELS_PATH", + "TETRATE_HOST", + "AVIAN_HOST", + // Observability Settings + "otel_exporter_otlp_endpoint", + "otel_exporter_otlp_timeout", + // Tunnel Settings + "tunnel_auto_start", + ]; + + pub const fn has_key(key: &str) -> bool { + let key_bytes = key.as_bytes(); + let mut i = 0; + while i < Self::ALL_KEYS.len() { + let candidate = Self::ALL_KEYS[i].as_bytes(); + if candidate.len() == key_bytes.len() { + let mut j = 0; + let mut eq = true; + while j < key_bytes.len() { + if candidate[j] != key_bytes[j] { + eq = false; + break; + } + j += 1; + } + if eq { + return true; + } + } + i += 1; + } + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + use schemars::schema_for; + + #[test] + fn all_keys_matches_struct_fields() { + let schema = schema_for!(GooseConfigSchema); + let obj = schema.as_object().expect("schema should be an object"); + let properties = obj + .get("properties") + .and_then(|p| p.as_object()) + .expect("schema should have properties"); + + let schema_keys: std::collections::HashSet<&str> = + properties.keys().map(|k| k.as_str()).collect(); + + for key in GooseConfigSchema::ALL_KEYS { + assert!( + schema_keys.contains(key), + "ALL_KEYS contains '{key}' but GooseConfigSchema has no field with serde(rename = \"{key}\")" + ); + } + + // Category B keys are in the struct but NOT in ALL_KEYS — that's intentional + let category_b = ["extensions", "slash_commands", "experiments"]; + for key in &category_b { + assert!( + schema_keys.contains(key), + "Category B key '{key}' should be in the schema struct for IDE autocomplete" + ); + assert!( + !GooseConfigSchema::has_key(key), + "Category B key '{key}' should NOT be in ALL_KEYS" + ); + } + } +} diff --git a/crates/goose/src/context_mgmt/mod.rs b/crates/goose/src/context_mgmt/mod.rs index 1cde777b04a9..e7bebfa47743 100644 --- a/crates/goose/src/context_mgmt/mod.rs +++ b/crates/goose/src/context_mgmt/mod.rs @@ -22,7 +22,7 @@ const TOOLCALL_SUMMARIZATION_BATCH_SIZE: usize = 10; fn tool_pair_summarization_enabled() -> bool { Config::global() - .get_param::("GOOSE_TOOL_PAIR_SUMMARIZATION") + .get_goose_tool_pair_summarization() .unwrap_or(true) } @@ -196,7 +196,7 @@ pub async fn check_if_compaction_needed( let config = Config::global(); let threshold = threshold_override.unwrap_or_else(|| { config - .get_param::("GOOSE_AUTO_COMPACT_THRESHOLD") + .get_goose_auto_compact_threshold() .unwrap_or(DEFAULT_COMPACTION_THRESHOLD) }); diff --git a/crates/goose/src/dictation/providers.rs b/crates/goose/src/dictation/providers.rs index 2d2378970b61..c963190247b5 100644 --- a/crates/goose/src/dictation/providers.rs +++ b/crates/goose/src/dictation/providers.rs @@ -210,13 +210,13 @@ fn build_api_client(provider: DictationProvider) -> Result<(ApiClient, String)> })?; let (base_url, query_params, endpoint_path) = if provider == DictationProvider::OpenAI { - let openai_base_url = config.get_param::("OPENAI_BASE_URL").ok(); + let openai_base_url = config.get_openai_base_url().ok(); if let Ok(host) = std::env::var("OPENAI_HOST") { (host, vec![], def.endpoint_path.to_string()) } else if let Some(target) = resolve_openai_base_url_target(openai_base_url.as_deref())? { target - } else if let Ok(host) = config.get_param::("OPENAI_HOST") { + } else if let Ok(host) = config.get_openai_host() { (host, vec![], def.endpoint_path.to_string()) } else { ( diff --git a/crates/goose/src/hints/load_hints.rs b/crates/goose/src/hints/load_hints.rs index 4050b530bd66..c694d5ee65b6 100644 --- a/crates/goose/src/hints/load_hints.rs +++ b/crates/goose/src/hints/load_hints.rs @@ -14,7 +14,7 @@ pub fn get_context_filenames() -> Vec { use crate::config::Config; Config::global() - .get_param::>("CONTEXT_FILE_NAMES") + .get_context_file_names() .unwrap_or_else(|_| { vec![ GOOSE_HINTS_FILENAME.to_string(), diff --git a/crates/goose/src/model.rs b/crates/goose/src/model.rs index eebace1aa8d0..d6906086b715 100644 --- a/crates/goose/src/model.rs +++ b/crates/goose/src/model.rs @@ -88,7 +88,7 @@ impl ModelConfig { None } } else { - match crate::config::Config::global().get_param::("GOOSE_CONTEXT_LIMIT") { + match crate::config::Config::global().get_goose_context_limit() { Ok(limit) => { if limit == 0 { return Err(ConfigError::InvalidRange( @@ -213,7 +213,7 @@ impl ModelConfig { } fn parse_max_tokens() -> Result, ConfigError> { - match crate::config::Config::global().get_param::("GOOSE_MAX_TOKENS") { + match crate::config::Config::global().get_goose_max_tokens() { Ok(tokens) => { if tokens <= 0 { return Err(ConfigError::InvalidRange( diff --git a/crates/goose/src/otel/otlp.rs b/crates/goose/src/otel/otlp.rs index 67d6c8bbcfd0..e3a7f5f98e96 100644 --- a/crates/goose/src/otel/otlp.rs +++ b/crates/goose/src/otel/otlp.rs @@ -83,12 +83,12 @@ pub fn signal_exporter(signal: &str) -> Option { /// Promotes goose config-file OTel settings to env vars before exporter build. pub fn promote_config_to_env(config: &crate::config::Config) { if env::var("OTEL_EXPORTER_OTLP_ENDPOINT").is_err() { - if let Ok(endpoint) = config.get_param::("otel_exporter_otlp_endpoint") { + if let Ok(endpoint) = config.get_otel_exporter_otlp_endpoint() { env::set_var("OTEL_EXPORTER_OTLP_ENDPOINT", endpoint); } } if env::var("OTEL_EXPORTER_OTLP_TIMEOUT").is_err() { - if let Ok(timeout) = config.get_param::("otel_exporter_otlp_timeout") { + if let Ok(timeout) = config.get_otel_exporter_otlp_timeout() { env::set_var("OTEL_EXPORTER_OTLP_TIMEOUT", timeout.to_string()); } } diff --git a/crates/goose/src/posthog.rs b/crates/goose/src/posthog.rs index 6f54c26e7ba2..0e2eb6208dd8 100644 --- a/crates/goose/src/posthog.rs +++ b/crates/goose/src/posthog.rs @@ -16,9 +16,6 @@ use uuid::Uuid; const POSTHOG_API_KEY: &str = "phc_RyX5CaY01VtZJCQyhSR5KFh6qimUy81YwxsEpotAftT"; const POSTHOG_CAPTURE_URL: &str = "https://us.i.posthog.com/capture/"; -/// Config key for telemetry opt-out preference -pub const TELEMETRY_ENABLED_KEY: &str = "GOOSE_TELEMETRY_ENABLED"; - static TELEMETRY_DISABLED_BY_ENV: Lazy = Lazy::new(|| { std::env::var("GOOSE_TELEMETRY_OFF") .map(|v| v == "1" || v.to_lowercase() == "true") @@ -36,7 +33,7 @@ pub fn get_telemetry_choice() -> Option { } let config = Config::global(); - config.get_param::(TELEMETRY_ENABLED_KEY).ok() + config.get_goose_telemetry_enabled().ok() } /// Check if telemetry is enabled. @@ -355,10 +352,10 @@ async fn send_error_event( } let config = Config::global(); - if let Ok(provider) = config.get_param::("GOOSE_PROVIDER") { + if let Ok(provider) = config.get_goose_provider() { insert(&mut props, "provider", provider); } - if let Ok(model) = config.get_param::("GOOSE_MODEL") { + if let Ok(model) = config.get_goose_model() { insert(&mut props, "model", model); } @@ -407,17 +404,17 @@ async fn send_session_event(installation: &InstallationData) -> Result<(), Strin insert(&mut props, "days_since_install", days_since_install); let config = Config::global(); - if let Ok(provider) = config.get_param::("GOOSE_PROVIDER") { + if let Ok(provider) = config.get_goose_provider() { insert(&mut props, "provider", provider); } - if let Ok(model) = config.get_param::("GOOSE_MODEL") { + if let Ok(model) = config.get_goose_model() { insert(&mut props, "model", model); } - if let Ok(mode) = config.get_param::("GOOSE_MODE") { - insert(&mut props, "setting_mode", mode); + if let Ok(mode) = config.get_goose_mode() { + insert(&mut props, "setting_mode", mode.to_string()); } - if let Ok(max_turns) = config.get_param::("GOOSE_MAX_TURNS") { + if let Some(max_turns) = config.get_goose_max_turns().ok().map(|v| v as i64) { insert(&mut props, "setting_max_turns", max_turns); } diff --git a/crates/goose/src/providers/anthropic.rs b/crates/goose/src/providers/anthropic.rs index 2a97b2ade7c2..0e3b7b147498 100644 --- a/crates/goose/src/providers/anthropic.rs +++ b/crates/goose/src/providers/anthropic.rs @@ -67,7 +67,7 @@ impl AnthropicProvider { let config = crate::config::Config::global(); let api_key: String = config.get_secret("ANTHROPIC_API_KEY")?; let host: String = config - .get_param("ANTHROPIC_HOST") + .get_anthropic_host() .unwrap_or_else(|_| "https://api.anthropic.com".to_string()); let auth = AuthMethod::ApiKey { @@ -248,7 +248,7 @@ impl ProviderDef for AnthropicProvider { .with_public( "host", config - .get_param::("ANTHROPIC_HOST") + .get_anthropic_host() .unwrap_or_else(|_| "https://api.anthropic.com".to_string()), ); diff --git a/crates/goose/src/providers/api_client.rs b/crates/goose/src/providers/api_client.rs index c0e4bbd136d5..302e17dd56a9 100644 --- a/crates/goose/src/providers/api_client.rs +++ b/crates/goose/src/providers/api_client.rs @@ -58,8 +58,8 @@ impl TlsConfig { let mut tls_config = TlsConfig::new(); let mut has_tls_config = false; - let client_cert_path = config.get_param::("GOOSE_CLIENT_CERT_PATH").ok(); - let client_key_path = config.get_param::("GOOSE_CLIENT_KEY_PATH").ok(); + let client_cert_path = config.get_goose_client_cert_path().ok(); + let client_key_path = config.get_goose_client_key_path().ok(); // Validate that both cert and key are provided if either is provided match (client_cert_path, client_key_path) { @@ -83,7 +83,7 @@ impl TlsConfig { (None, None) => {} } - if let Ok(ca_cert_path) = config.get_param::("GOOSE_CA_CERT_PATH") { + if let Ok(ca_cert_path) = config.get_goose_ca_cert_path() { tls_config = tls_config.with_ca_cert(std::path::PathBuf::from(ca_cert_path)); has_tls_config = true; } diff --git a/crates/goose/src/providers/avian.rs b/crates/goose/src/providers/avian.rs index 6efcd1f12575..2fce96fb909d 100644 --- a/crates/goose/src/providers/avian.rs +++ b/crates/goose/src/providers/avian.rs @@ -44,7 +44,7 @@ impl ProviderDef for AvianProvider { let config = crate::config::Config::global(); let api_key: String = config.get_secret("AVIAN_API_KEY")?; let host: String = config - .get_param("AVIAN_HOST") + .get_avian_host() .unwrap_or_else(|_| AVIAN_API_HOST.to_string()); let api_client = ApiClient::new(host, AuthMethod::BearerToken(api_key))?; diff --git a/crates/goose/src/providers/azure.rs b/crates/goose/src/providers/azure.rs index 4072ae68234d..4b2a3fc96429 100644 --- a/crates/goose/src/providers/azure.rs +++ b/crates/goose/src/providers/azure.rs @@ -75,10 +75,10 @@ impl ProviderDef for AzureProvider { ) -> BoxFuture<'static, Result> { Box::pin(async move { let config = crate::config::Config::global(); - let endpoint: String = config.get_param("AZURE_OPENAI_ENDPOINT")?; - let deployment_name: String = config.get_param("AZURE_OPENAI_DEPLOYMENT_NAME")?; + let endpoint: String = config.get_azure_openai_endpoint()?; + let deployment_name: String = config.get_azure_openai_deployment_name()?; let api_version: String = config - .get_param("AZURE_OPENAI_API_VERSION") + .get_azure_openai_api_version() .unwrap_or_else(|_| AZURE_DEFAULT_API_VERSION.to_string()); let api_key = config diff --git a/crates/goose/src/providers/bedrock.rs b/crates/goose/src/providers/bedrock.rs index a8b4fc3c3684..58939b91270b 100644 --- a/crates/goose/src/providers/bedrock.rs +++ b/crates/goose/src/providers/bedrock.rs @@ -91,7 +91,7 @@ impl BedrockProvider { }; // Get AWS_REGION from config if explicitly set (optional - SDK can resolve from other sources) - let region = match config.get_param::("AWS_REGION") { + let region = match config.get_aws_region() { Ok(r) if !r.is_empty() => Some(r), Ok(_) => None, Err(_) => None, @@ -100,7 +100,7 @@ impl BedrockProvider { // Use load_defaults() which supports AWS SSO, profiles, and environment variables let mut loader = aws_config::defaults(aws_config::BehaviorVersion::latest()); - if let Ok(profile_name) = config.get_param::("AWS_PROFILE") { + if let Ok(profile_name) = config.get_aws_profile() { if !profile_name.is_empty() { loader = loader.profile_name(&profile_name); } @@ -166,19 +166,19 @@ impl BedrockProvider { fn load_retry_config(config: &crate::config::Config) -> RetryConfig { let max_retries = config - .get_param::("BEDROCK_MAX_RETRIES") + .get_bedrock_max_retries() .unwrap_or(BEDROCK_DEFAULT_MAX_RETRIES); let initial_interval_ms = config - .get_param::("BEDROCK_INITIAL_RETRY_INTERVAL_MS") + .get_bedrock_initial_retry_interval_ms() .unwrap_or(BEDROCK_DEFAULT_INITIAL_RETRY_INTERVAL_MS); let backoff_multiplier = config - .get_param::("BEDROCK_BACKOFF_MULTIPLIER") + .get_bedrock_backoff_multiplier() .unwrap_or(BEDROCK_DEFAULT_BACKOFF_MULTIPLIER); let max_interval_ms = config - .get_param::("BEDROCK_MAX_RETRY_INTERVAL_MS") + .get_bedrock_max_retry_interval_ms() .unwrap_or(BEDROCK_DEFAULT_MAX_RETRY_INTERVAL_MS); RetryConfig::new( @@ -192,9 +192,7 @@ impl BedrockProvider { fn should_enable_caching(&self) -> bool { let config = crate::config::Config::global(); - let enabled = config - .get_param::("BEDROCK_ENABLE_CACHING") - .unwrap_or(false); + let enabled = config.get_bedrock_enable_caching().unwrap_or(false); enabled && self.model.model_name.contains("anthropic.claude") } diff --git a/crates/goose/src/providers/databricks.rs b/crates/goose/src/providers/databricks.rs index 5fc7b09ac544..578dffb6456b 100644 --- a/crates/goose/src/providers/databricks.rs +++ b/crates/goose/src/providers/databricks.rs @@ -145,7 +145,7 @@ impl DatabricksProvider { pub async fn from_env(model: ModelConfig) -> Result { let config = crate::config::Config::global(); - let mut host: Result = config.get_param("DATABRICKS_HOST"); + let mut host: Result = config.get_databricks_host(); if host.is_err() { host = config.get_secret("DATABRICKS_HOST") } @@ -198,25 +198,25 @@ impl DatabricksProvider { fn load_retry_config(config: &crate::config::Config) -> RetryConfig { let max_retries = config - .get_param("DATABRICKS_MAX_RETRIES") + .get_databricks_max_retries() .ok() .and_then(|v: String| v.parse::().ok()) .unwrap_or(DEFAULT_MAX_RETRIES); let initial_interval_ms = config - .get_param("DATABRICKS_INITIAL_RETRY_INTERVAL_MS") + .get_databricks_initial_retry_interval_ms() .ok() .and_then(|v: String| v.parse::().ok()) .unwrap_or(DEFAULT_INITIAL_RETRY_INTERVAL_MS); let backoff_multiplier = config - .get_param("DATABRICKS_BACKOFF_MULTIPLIER") + .get_databricks_backoff_multiplier() .ok() .and_then(|v: String| v.parse::().ok()) .unwrap_or(DEFAULT_BACKOFF_MULTIPLIER); let max_interval_ms = config - .get_param("DATABRICKS_MAX_RETRY_INTERVAL_MS") + .get_databricks_max_retry_interval_ms() .ok() .and_then(|v: String| v.parse::().ok()) .unwrap_or(DEFAULT_MAX_RETRY_INTERVAL_MS); @@ -259,7 +259,7 @@ impl DatabricksProvider { fn resolve_instance_id() -> Option { let enabled = crate::config::Config::global() - .get_param::("GOOSE_DATABRICKS_CLIENT_REQUEST_ID") + .get_goose_databricks_client_request_id() .unwrap_or(false); if enabled { Some(get_instance_id().to_string()) diff --git a/crates/goose/src/providers/gcpvertexai.rs b/crates/goose/src/providers/gcpvertexai.rs index 24aed27c0561..5977697b909d 100644 --- a/crates/goose/src/providers/gcpvertexai.rs +++ b/crates/goose/src/providers/gcpvertexai.rs @@ -166,7 +166,7 @@ impl GcpVertexAIProvider { /// * `model` - Configuration for the model to be used pub async fn from_env(model: ModelConfig) -> Result { let config = crate::config::Config::global(); - let project_id = config.get_param("GCP_PROJECT_ID")?; + let project_id = config.get_gcp_project_id()?; let location = Self::determine_location(config)?; let host = Self::build_host_url(&location); @@ -195,25 +195,25 @@ impl GcpVertexAIProvider { fn load_retry_config(config: &crate::config::Config) -> RetryConfig { // Load max retries for 429 rate limit errors let max_retries = config - .get_param("GCP_MAX_RETRIES") + .get_gcp_max_retries() .ok() .and_then(|v: String| v.parse::().ok()) .unwrap_or(DEFAULT_MAX_RETRIES); let initial_interval_ms = config - .get_param("GCP_INITIAL_RETRY_INTERVAL_MS") + .get_gcp_initial_retry_interval_ms() .ok() .and_then(|v: String| v.parse::().ok()) .unwrap_or(DEFAULT_INITIAL_RETRY_INTERVAL_MS); let backoff_multiplier = config - .get_param("GCP_BACKOFF_MULTIPLIER") + .get_gcp_backoff_multiplier() .ok() .and_then(|v: String| v.parse::().ok()) .unwrap_or(DEFAULT_BACKOFF_MULTIPLIER); let max_interval_ms = config - .get_param("GCP_MAX_RETRY_INTERVAL_MS") + .get_gcp_max_retry_interval_ms() .ok() .and_then(|v: String| v.parse::().ok()) .unwrap_or(DEFAULT_MAX_RETRY_INTERVAL_MS); @@ -233,7 +233,7 @@ impl GcpVertexAIProvider { /// 2. Global default location (Iowa) fn determine_location(config: &crate::config::Config) -> Result { Ok(config - .get_param("GCP_LOCATION") + .get_gcp_location() .ok() .filter(|location: &String| !location.trim().is_empty()) .unwrap_or_else(|| GcpLocation::Iowa.to_string())) diff --git a/crates/goose/src/providers/githubcopilot.rs b/crates/goose/src/providers/githubcopilot.rs index c65de1637ec6..c9e4630b3395 100644 --- a/crates/goose/src/providers/githubcopilot.rs +++ b/crates/goose/src/providers/githubcopilot.rs @@ -182,7 +182,7 @@ impl GithubCopilotProvider { let config = Config::global(); let host = normalize_host( &config - .get_param::("GITHUB_COPILOT_HOST") + .get_github_copilot_host() .unwrap_or_else(|_| DEFAULT_GITHUB_HOST.to_string()), ); DiskCache::new(&host).clear().await @@ -215,13 +215,13 @@ impl GithubCopilotProvider { let config = Config::global(); let host = normalize_host( &config - .get_param::("GITHUB_COPILOT_HOST") + .get_github_copilot_host() .unwrap_or_else(|_| DEFAULT_GITHUB_HOST.to_string()), ); let client_id: String = config - .get_param("GITHUB_COPILOT_CLIENT_ID") + .get_github_copilot_client_id() .unwrap_or_else(|_| DEFAULT_GITHUB_COPILOT_CLIENT_ID.to_string()); - let copilot_token_url: Option = config.get_param("GITHUB_COPILOT_TOKEN_URL").ok(); + let copilot_token_url: Option = config.get_github_copilot_token_url().ok(); let urls = GithubCopilotUrls::new(&host, copilot_token_url.as_deref()); let client = Client::builder() .timeout(Duration::from_secs(600)) diff --git a/crates/goose/src/providers/google.rs b/crates/goose/src/providers/google.rs index 6b2e894bba2a..f09a8a139624 100644 --- a/crates/goose/src/providers/google.rs +++ b/crates/goose/src/providers/google.rs @@ -73,7 +73,7 @@ impl GoogleProvider { let config = crate::config::Config::global(); let api_key: String = config.get_secret("GOOGLE_API_KEY")?; let host: String = config - .get_param("GOOGLE_HOST") + .get_google_host() .unwrap_or_else(|_| GOOGLE_API_HOST.to_string()); let auth = AuthMethod::ApiKey { @@ -147,7 +147,7 @@ impl ProviderDef for GoogleProvider { .with_public( "host", config - .get_param::("GOOGLE_HOST") + .get_google_host() .unwrap_or_else(|_| GOOGLE_API_HOST.to_string()), ); diff --git a/crates/goose/src/providers/litellm.rs b/crates/goose/src/providers/litellm.rs index 8008e0659ec6..4f72fd180f7c 100644 --- a/crates/goose/src/providers/litellm.rs +++ b/crates/goose/src/providers/litellm.rs @@ -39,16 +39,16 @@ impl LiteLLMProvider { .unwrap_or_default(); let api_key = secrets.get("LITELLM_API_KEY").cloned().unwrap_or_default(); let host: String = config - .get_param("LITELLM_HOST") + .get_litellm_host() .unwrap_or_else(|_| "https://api.litellm.ai".to_string()); let base_path: String = config - .get_param("LITELLM_BASE_PATH") + .get_litellm_base_path() .unwrap_or_else(|_| "v1/chat/completions".to_string()); let custom_headers: Option> = secrets .get("LITELLM_CUSTOM_HEADERS") .cloned() .map(parse_custom_headers); - let timeout_secs: u64 = config.get_param("LITELLM_TIMEOUT").unwrap_or(600); + let timeout_secs: u64 = config.get_litellm_timeout().unwrap_or(600); let auth = if api_key.is_empty() { AuthMethod::NoAuth diff --git a/crates/goose/src/providers/ollama.rs b/crates/goose/src/providers/ollama.rs index d397cf50721a..1fb1847f0424 100644 --- a/crates/goose/src/providers/ollama.rs +++ b/crates/goose/src/providers/ollama.rs @@ -56,7 +56,7 @@ pub struct OllamaProvider { } fn resolve_ollama_num_ctx(model_config: &ModelConfig) -> Option { let config = crate::config::Config::global(); - let input_limit = match config.get_param::("GOOSE_INPUT_LIMIT") { + let input_limit = match config.get_goose_input_limit() { Ok(limit) if limit > 0 => Some(limit), Ok(_) => None, Err(crate::config::ConfigError::NotFound(_)) => None, @@ -71,7 +71,7 @@ fn resolve_ollama_num_ctx(model_config: &ModelConfig) -> Option { fn resolve_ollama_stream_usage() -> bool { let config = crate::config::Config::global(); - match config.get_param::("OLLAMA_STREAM_USAGE") { + match config.get_ollama_stream_usage() { Ok(val) => val, // Key not set: default to true. Ollama supports stream_options since // mid-2025 and most installs benefit from token usage tracking. @@ -123,18 +123,18 @@ fn apply_ollama_options(payload: &mut Value, model_config: &ModelConfig) { } fn ollama_host_configured(config: &crate::config::Config) -> bool { - config.get_param::("OLLAMA_HOST").is_ok() + config.get_ollama_host().is_ok() } impl OllamaProvider { pub async fn from_env(model: ModelConfig) -> Result { let config = crate::config::Config::global(); let host: String = config - .get_param("OLLAMA_HOST") + .get_ollama_host() .unwrap_or_else(|_| OLLAMA_HOST.to_string()); let timeout: Duration = - Duration::from_secs(config.get_param("OLLAMA_TIMEOUT").unwrap_or(OLLAMA_TIMEOUT)); + Duration::from_secs(config.get_ollama_timeout().unwrap_or(OLLAMA_TIMEOUT)); let base = if host.starts_with("http://") || host.starts_with("https://") { host.clone() @@ -276,7 +276,7 @@ impl ProviderDef for OllamaProvider { InventoryIdentityInput::new(OLLAMA_PROVIDER_NAME, OLLAMA_PROVIDER_NAME).with_public( "host", config - .get_param::("OLLAMA_HOST") + .get_ollama_host() .unwrap_or_else(|_| OLLAMA_HOST.to_string()), ), ) @@ -391,17 +391,17 @@ const OLLAMA_DEFAULT_CHUNK_TIMEOUT_SECS: u64 = 120; fn resolve_ollama_chunk_timeout() -> u64 { let config = crate::config::Config::global(); - if let Ok(val) = config.get_param::("OLLAMA_STREAM_TIMEOUT") { + if let Ok(val) = config.get_ollama_stream_timeout() { if val > 0 { return val; } } - if let Ok(val) = config.get_param::("GOOSE_STREAM_TIMEOUT") { + if let Ok(val) = config.get_goose_stream_timeout() { if val > 0 { return val; } } - match config.get_param::("OLLAMA_TIMEOUT") { + match config.get_ollama_timeout() { Ok(val) if val > 0 => val, _ => OLLAMA_DEFAULT_CHUNK_TIMEOUT_SECS, } diff --git a/crates/goose/src/providers/openai.rs b/crates/goose/src/providers/openai.rs index c8c531aa7e55..39b837e6a61d 100644 --- a/crates/goose/src/providers/openai.rs +++ b/crates/goose/src/providers/openai.rs @@ -156,7 +156,7 @@ impl OpenAiProvider { from_base_url: false, } } else if let Some(raw_url) = config - .get_param::("OPENAI_BASE_URL") + .get_openai_base_url() .ok() .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) @@ -164,7 +164,7 @@ impl OpenAiProvider { Self::parse_base_url(&raw_url)? } else { let h: String = config - .get_param("OPENAI_HOST") + .get_openai_host() .unwrap_or_else(|_| "https://api.openai.com".to_string()); ParsedBaseUrl { host: h, @@ -191,7 +191,7 @@ impl OpenAiProvider { std::env::var("OPENAI_BASE_PATH").unwrap_or_else(|_| default_bp()) } else { config - .get_param("OPENAI_BASE_PATH") + .get_openai_base_path() .unwrap_or_else(|_| default_bp()) }; @@ -227,9 +227,9 @@ impl OpenAiProvider { .cloned() .map(parse_custom_headers); - let organization: Option = config.get_param("OPENAI_ORGANIZATION").ok(); - let project: Option = config.get_param("OPENAI_PROJECT").ok(); - let timeout_secs: u64 = config.get_param("OPENAI_TIMEOUT").unwrap_or(600); + let organization: Option = config.get_openai_organization().ok(); + let project: Option = config.get_openai_project().ok(); + let timeout_secs: u64 = config.get_openai_timeout().unwrap_or(600); let auth = match api_key { Some(key) if !key.is_empty() => AuthMethod::BearerToken(key), @@ -588,9 +588,16 @@ impl ProviderDef for OpenAiProvider { fn inventory_configured() -> bool { let config = crate::config::Config::global(); + if config + .get_openai_base_url() + .ok() + .is_some_and(|base_url| !base_url.trim().is_empty()) + { + return true; + } // If the host is explicitly set to something non-default, trust the user's // custom setup (e.g. a local server that doesn't require an API key). - if let Ok(host) = config.get_param::("OPENAI_HOST") { + if let Ok(host) = config.get_openai_host() { if host != "https://api.openai.com" { return true; } @@ -603,25 +610,38 @@ impl ProviderDef for OpenAiProvider { fn inventory_identity() -> Result { let config = crate::config::Config::global(); + let (host, base_path) = if let Some(raw_url) = config + .get_openai_base_url() + .ok() + .filter(|base_url| !base_url.trim().is_empty()) + { + let parsed = Self::parse_base_url(&raw_url)?; + let base_path = if parsed.has_v1 { + OPEN_AI_DEFAULT_BASE_PATH.to_string() + } else { + OPEN_AI_VERSIONLESS_BASE_PATH.to_string() + }; + (parsed.host, base_path) + } else { + ( + config + .get_openai_host() + .unwrap_or_else(|_| "https://api.openai.com".to_string()), + config + .get_openai_base_path() + .unwrap_or_else(|_| OPEN_AI_DEFAULT_BASE_PATH.to_string()), + ) + }; + let mut identity = InventoryIdentityInput::new(OPEN_AI_PROVIDER_NAME, OPEN_AI_PROVIDER_NAME) - .with_public( - "host", - config - .get_param::("OPENAI_HOST") - .unwrap_or_else(|_| "https://api.openai.com".to_string()), - ) - .with_public( - "base_path", - config - .get_param::("OPENAI_BASE_PATH") - .unwrap_or_else(|_| OPEN_AI_DEFAULT_BASE_PATH.to_string()), - ); + .with_public("host", host) + .with_public("base_path", base_path); - if let Ok(organization) = config.get_param::("OPENAI_ORGANIZATION") { + if let Ok(organization) = config.get_openai_organization() { identity = identity.with_public("organization", organization); } - if let Ok(project) = config.get_param::("OPENAI_PROJECT") { + if let Ok(project) = config.get_openai_project() { identity = identity.with_public("project", project); } if let Some(api_key) = config_secret_value(config, "OPENAI_API_KEY") { diff --git a/crates/goose/src/providers/openrouter.rs b/crates/goose/src/providers/openrouter.rs index 08b8689b99fd..89e1fe8a342f 100644 --- a/crates/goose/src/providers/openrouter.rs +++ b/crates/goose/src/providers/openrouter.rs @@ -52,7 +52,7 @@ impl OpenRouterProvider { let config = crate::config::Config::global(); let api_key: String = config.get_secret("OPENROUTER_API_KEY")?; let host: String = config - .get_param("OPENROUTER_HOST") + .get_openrouter_host() .unwrap_or_else(|_| "https://openrouter.ai".to_string()); let auth = AuthMethod::BearerToken(api_key); diff --git a/crates/goose/src/providers/sagemaker_tgi.rs b/crates/goose/src/providers/sagemaker_tgi.rs index 89fe5424cf2d..144ada825204 100644 --- a/crates/goose/src/providers/sagemaker_tgi.rs +++ b/crates/goose/src/providers/sagemaker_tgi.rs @@ -44,7 +44,7 @@ impl SageMakerTgiProvider { let config = crate::config::Config::global(); // Get SageMaker endpoint name (just the name, not full URL) - let endpoint_name: String = config.get_param("SAGEMAKER_ENDPOINT_NAME").map_err(|_| { + let endpoint_name: String = config.get_sagemaker_endpoint_name().map_err(|_| { anyhow::anyhow!("SAGEMAKER_ENDPOINT_NAME is required for SageMaker TGI provider") })?; diff --git a/crates/goose/src/providers/snowflake.rs b/crates/goose/src/providers/snowflake.rs index 6dc374289479..b4d725c7c53f 100644 --- a/crates/goose/src/providers/snowflake.rs +++ b/crates/goose/src/providers/snowflake.rs @@ -60,7 +60,7 @@ pub struct SnowflakeProvider { impl SnowflakeProvider { pub async fn from_env(model: ModelConfig) -> Result { let config = crate::config::Config::global(); - let mut host: Result = config.get_param("SNOWFLAKE_HOST"); + let mut host: Result = config.get_snowflake_host(); if host.is_err() { host = config.get_secret("SNOWFLAKE_HOST") } diff --git a/crates/goose/src/providers/tetrate.rs b/crates/goose/src/providers/tetrate.rs index 810246168f64..9f8f5878dd00 100644 --- a/crates/goose/src/providers/tetrate.rs +++ b/crates/goose/src/providers/tetrate.rs @@ -50,7 +50,7 @@ impl TetrateProvider { let config = crate::config::Config::global(); let api_key: String = config.get_secret("TETRATE_API_KEY")?; let host: String = config - .get_param("TETRATE_HOST") + .get_tetrate_host() .unwrap_or_else(|_| "https://api.router.tetrate.ai".to_string()); let auth = AuthMethod::BearerToken(api_key); diff --git a/crates/goose/src/providers/venice.rs b/crates/goose/src/providers/venice.rs index 5440632eafc4..f4c31beec3e7 100644 --- a/crates/goose/src/providers/venice.rs +++ b/crates/goose/src/providers/venice.rs @@ -91,13 +91,13 @@ impl VeniceProvider { let config = crate::config::Config::global(); let api_key: String = config.get_secret("VENICE_API_KEY")?; let host: String = config - .get_param("VENICE_HOST") + .get_venice_host() .unwrap_or_else(|_| VENICE_DEFAULT_HOST.to_string()); let base_path: String = config - .get_param("VENICE_BASE_PATH") + .get_venice_base_path() .unwrap_or_else(|_| VENICE_DEFAULT_BASE_PATH.to_string()); let models_path: String = config - .get_param("VENICE_MODELS_PATH") + .get_venice_models_path() .unwrap_or_else(|_| VENICE_DEFAULT_MODELS_PATH.to_string()); let auth = AuthMethod::BearerToken(api_key); diff --git a/crates/goose/src/providers/xai.rs b/crates/goose/src/providers/xai.rs index 7006ad39066b..66de7aec0368 100644 --- a/crates/goose/src/providers/xai.rs +++ b/crates/goose/src/providers/xai.rs @@ -59,7 +59,7 @@ impl ProviderDef for XaiProvider { let config = crate::config::Config::global(); let api_key: String = config.get_secret("XAI_API_KEY")?; let host: String = config - .get_param("XAI_HOST") + .get_xai_host() .unwrap_or_else(|_| XAI_API_HOST.to_string()); let api_client = ApiClient::new(host, AuthMethod::BearerToken(api_key))?; diff --git a/crates/goose/src/security/mod.rs b/crates/goose/src/security/mod.rs index c6938f2ceb7b..d68eb7ad3667 100644 --- a/crates/goose/src/security/mod.rs +++ b/crates/goose/src/security/mod.rs @@ -61,20 +61,18 @@ impl SecurityManager { pub fn is_prompt_injection_detection_enabled(&self) -> bool { let config = Config::global(); - config - .get_param::("SECURITY_PROMPT_ENABLED") - .unwrap_or(false) + config.get_security_prompt_enabled().unwrap_or(false) } fn is_ml_scanning_enabled(&self) -> bool { let config = Config::global(); let prompt_enabled = config - .get_param::("SECURITY_PROMPT_CLASSIFIER_ENABLED") + .get_security_prompt_classifier_enabled() .unwrap_or(false); let command_enabled = config - .get_param::("SECURITY_COMMAND_CLASSIFIER_ENABLED") + .get_security_command_classifier_enabled() .unwrap_or(false); prompt_enabled || command_enabled @@ -96,10 +94,10 @@ impl SecurityManager { let scanner = self.scanner.get_or_init(|| { let config = Config::global(); let command_classifier_enabled = config - .get_param::("SECURITY_COMMAND_CLASSIFIER_ENABLED") + .get_security_command_classifier_enabled() .unwrap_or(false); let prompt_classifier_enabled = config - .get_param::("SECURITY_PROMPT_CLASSIFIER_ENABLED") + .get_security_prompt_classifier_enabled() .unwrap_or(false); tracing::info!( diff --git a/crates/goose/src/security/scanner.rs b/crates/goose/src/security/scanner.rs index 8d0e4e4c4c4c..727f51f594b3 100644 --- a/crates/goose/src/security/scanner.rs +++ b/crates/goose/src/security/scanner.rs @@ -114,7 +114,7 @@ impl PromptInjectionScanner { pub fn get_threshold_from_config(&self) -> f32 { Config::global() - .get_param::("SECURITY_PROMPT_THRESHOLD") + .get_security_prompt_threshold() .unwrap_or(0.8) as f32 } diff --git a/crates/goose/src/slash_commands.rs b/crates/goose/src/slash_commands.rs index 5e7065db0016..d17bf5fb6ff3 100644 --- a/crates/goose/src/slash_commands.rs +++ b/crates/goose/src/slash_commands.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use anyhow::Result; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use tracing::warn; @@ -9,7 +10,7 @@ use crate::recipe::Recipe; const SLASH_COMMANDS_CONFIG_KEY: &str = "slash_commands"; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct SlashCommandMapping { pub command: String, pub recipe_path: String,