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 <wpfleger@block.xyz>
This commit is contained in:
Will Pfleger
2026-04-17 15:07:46 -04:00
parent 2ee9687741
commit e44a0cdc1a
52 changed files with 2275 additions and 199 deletions
+5
View File
@@ -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
+22
View File
@@ -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..."
+18 -18
View File
@@ -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<bool> {
.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::<String>("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",
@@ -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,
@@ -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<RecipeFile> {
fn configured_github_recipe_repo() -> Option<String> {
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,
}
+14 -13
View File
@@ -466,7 +466,7 @@ async fn configure_session_prompts(
.await;
}
let system_prompt_file: Option<String> = config.get_param("GOOSE_SYSTEM_PROMPT_FILE_PATH").ok();
let system_prompt_file: Option<String> = 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::<String>("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")),
+1 -1
View File
@@ -84,7 +84,7 @@ impl rustyline::ConditionalEventHandler for CtrlCHandler {
pub fn get_newline_key() -> char {
Config::global()
.get_param::<String>("GOOSE_CLI_NEWLINE_KEY")
.get_goose_cli_newline_key()
.ok()
.and_then(|s| s.chars().next())
.map(|c| c.to_ascii_lowercase())
+5 -7
View File
@@ -1455,9 +1455,7 @@ impl CliSession {
let context_limit = model_config.context_limit();
let config = Config::global();
let show_cost = config
.get_param::<bool>("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::<f32>("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::<f32>("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<Arc<dyn Provider>, 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::<String>("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<Arc<dyn Provider>, anyhow::Error> {
};
// Try planner-specific model first, fall back to default model
let model = if let Ok(model) = config.get_param::<String>("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...");
+10 -10
View File
@@ -37,10 +37,10 @@ impl Theme {
fn as_str(&self) -> String {
match self {
Theme::Light => Config::global()
.get_param::<String>("GOOSE_CLI_LIGHT_THEME")
.get_goose_cli_light_theme()
.unwrap_or(DEFAULT_CLI_LIGHT_THEME.to_string()),
Theme::Dark => Config::global()
.get_param::<String>("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::<String>("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<bool> = RefCell::new(
Config::global().get_param::<bool>("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::<String>("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::<bool>("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::<f32>("GOOSE_CLI_MIN_PRIORITY")
.get_goose_cli_min_priority()
.ok()
.unwrap_or(DEFAULT_MIN_PRIORITY);
+2 -4
View File
@@ -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<String> {
@@ -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))
}
+4
View File
@@ -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 = [
File diff suppressed because it is too large Load Diff
+2 -5
View File
@@ -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
+4 -5
View File
@@ -403,8 +403,7 @@ impl Agent {
self.tool_inspection_manager.apply_tool_annotations(&tools);
}
let tool_call_cut_off = match Config::global().get_param::<usize>("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::<f64>("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::<f64>("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::<u32>("GOOSE_MAX_TURNS")
.get_goose_max_turns()
.unwrap_or(DEFAULT_MAX_TURNS)
});
let mut compaction_attempts = 0;
+3 -2
View File
@@ -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<T> = Result<T, ExtensionError>;
#[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
@@ -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()
}
@@ -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::<usize>("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::<String>("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::<String>("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::<usize>("GOOSE_SUBAGENT_MAX_TURNS")
.ok()
})
.or_else(|| Config::global().get_goose_subagent_max_turns().ok())
.unwrap_or(DEFAULT_SUBAGENT_MAX_TURNS)
}
+2 -10
View File
@@ -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);
@@ -44,7 +44,7 @@ impl TaskConfig {
extensions,
max_turns: Some(
Config::global()
.get_param::<usize>("GOOSE_SUBAGENT_MAX_TURNS")
.get_goose_subagent_max_turns()
.unwrap_or(DEFAULT_SUBAGENT_MAX_TURNS),
),
}
@@ -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());
}
+134
View File
@@ -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 [<get_ $key:lower>](&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<String>);
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<String>);
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::*;
+2 -1
View File
@@ -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,
+2
View File
@@ -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")]
+1
View File
@@ -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;
+476
View File
@@ -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<String>,
#[serde(rename = "GOOSE_MODEL")]
pub goose_model: Option<String>,
#[serde(rename = "GOOSE_MODE")]
pub goose_mode: Option<GooseMode>,
#[serde(rename = "GOOSE_MAX_TOKENS")]
pub goose_max_tokens: Option<i32>,
#[serde(rename = "GOOSE_CONTEXT_LIMIT")]
pub goose_context_limit: Option<u64>,
#[serde(rename = "GOOSE_INPUT_LIMIT")]
pub goose_input_limit: Option<u64>,
#[serde(rename = "GOOSE_MAX_TURNS")]
pub goose_max_turns: Option<u32>,
#[serde(rename = "GOOSE_MAX_ACTIVE_AGENTS")]
pub goose_max_active_agents: Option<u64>,
#[serde(rename = "GOOSE_AUTO_COMPACT_THRESHOLD")]
pub goose_auto_compact_threshold: Option<f64>,
#[serde(rename = "GOOSE_TOOL_PAIR_SUMMARIZATION")]
pub goose_tool_pair_summarization: Option<bool>,
#[serde(rename = "GOOSE_TOOL_CALL_CUTOFF")]
pub goose_tool_call_cutoff: Option<u64>,
#[serde(rename = "GOOSE_STREAM_TIMEOUT")]
pub goose_stream_timeout: Option<u64>,
#[serde(rename = "GOOSE_SEARCH_PATHS")]
pub goose_search_paths: Option<Vec<String>>,
#[serde(rename = "GOOSE_DISABLE_SESSION_NAMING")]
pub goose_disable_session_naming: Option<bool>,
#[serde(rename = "GOOSE_DISABLE_KEYRING")]
pub goose_disable_keyring: Option<String>,
#[serde(rename = "GOOSE_TELEMETRY_ENABLED")]
pub goose_telemetry_enabled: Option<bool>,
#[serde(rename = "GOOSE_DEFAULT_EXTENSION_TIMEOUT")]
pub goose_default_extension_timeout: Option<u64>,
#[serde(rename = "GOOSE_PROMPT_EDITOR")]
pub goose_prompt_editor: Option<String>,
#[serde(rename = "GOOSE_PROMPT_EDITOR_ALWAYS")]
pub goose_prompt_editor_always: Option<bool>,
#[serde(rename = "GOOSE_ALLOWLIST")]
pub goose_allowlist: Option<String>,
#[serde(rename = "GOOSE_SYSTEM_PROMPT_FILE_PATH")]
pub goose_system_prompt_file_path: Option<String>,
#[serde(rename = "GOOSE_DEBUG")]
pub goose_debug: Option<bool>,
#[serde(rename = "GOOSE_SHOW_FULL_OUTPUT")]
pub goose_show_full_output: Option<bool>,
#[serde(rename = "GOOSE_STATUS_HOOK")]
pub goose_status_hook: Option<String>,
#[serde(rename = "GOOSE_LOCAL_ENABLE_THINKING")]
pub goose_local_enable_thinking: Option<bool>,
#[serde(rename = "GOOSE_DATABRICKS_CLIENT_REQUEST_ID")]
pub goose_databricks_client_request_id: Option<bool>,
#[serde(rename = "CONTEXT_FILE_NAMES")]
pub context_file_names: Option<Vec<String>>,
#[serde(rename = "EDIT_MODE")]
pub edit_mode: Option<String>,
#[serde(rename = "RANDOM_THINKING_MESSAGES")]
pub random_thinking_messages: Option<bool>,
#[serde(rename = "CODE_MODE_TOOL_DISCLOSURE")]
pub code_mode_tool_disclosure: Option<String>,
// === mTLS Settings ===
#[serde(rename = "GOOSE_CLIENT_CERT_PATH")]
pub goose_client_cert_path: Option<String>,
#[serde(rename = "GOOSE_CLIENT_KEY_PATH")]
pub goose_client_key_path: Option<String>,
#[serde(rename = "GOOSE_CA_CERT_PATH")]
pub goose_ca_cert_path: Option<String>,
// === Planner & Subagent Settings ===
#[serde(rename = "GOOSE_PLANNER_PROVIDER")]
pub goose_planner_provider: Option<String>,
#[serde(rename = "GOOSE_PLANNER_MODEL")]
pub goose_planner_model: Option<String>,
#[serde(rename = "GOOSE_SUBAGENT_PROVIDER")]
pub goose_subagent_provider: Option<String>,
#[serde(rename = "GOOSE_SUBAGENT_MODEL")]
pub goose_subagent_model: Option<String>,
#[serde(rename = "GOOSE_SUBAGENT_MAX_TURNS")]
pub goose_subagent_max_turns: Option<u64>,
#[serde(rename = "GOOSE_MAX_BACKGROUND_TASKS")]
pub goose_max_background_tasks: Option<u64>,
// === Recipe Settings ===
#[serde(rename = "GOOSE_RECIPE_GITHUB_REPO")]
pub goose_recipe_github_repo: Option<String>,
#[serde(rename = "GOOSE_RECIPE_RETRY_TIMEOUT_SECONDS")]
pub goose_recipe_retry_timeout_seconds: Option<u64>,
#[serde(rename = "GOOSE_RECIPE_ON_FAILURE_TIMEOUT_SECONDS")]
pub goose_recipe_on_failure_timeout_seconds: Option<u64>,
// === CLI Settings ===
#[serde(rename = "GOOSE_CLI_MIN_PRIORITY")]
pub goose_cli_min_priority: Option<f32>,
#[serde(rename = "GOOSE_CLI_THEME")]
pub goose_cli_theme: Option<String>,
#[serde(rename = "GOOSE_CLI_LIGHT_THEME")]
pub goose_cli_light_theme: Option<String>,
#[serde(rename = "GOOSE_CLI_DARK_THEME")]
pub goose_cli_dark_theme: Option<String>,
#[serde(rename = "GOOSE_CLI_SHOW_COST")]
pub goose_cli_show_cost: Option<bool>,
#[serde(rename = "GOOSE_CLI_SHOW_THINKING")]
pub goose_cli_show_thinking: Option<bool>,
#[serde(rename = "GOOSE_CLI_NEWLINE_KEY")]
pub goose_cli_newline_key: Option<String>,
// === AI Agent / Thinking Settings ===
#[serde(rename = "CLAUDE_CODE_COMMAND")]
pub claude_code_command: Option<String>,
#[serde(rename = "GEMINI_CLI_COMMAND")]
pub gemini_cli_command: Option<String>,
#[serde(rename = "CURSOR_AGENT_COMMAND")]
pub cursor_agent_command: Option<String>,
#[serde(rename = "CODEX_COMMAND")]
pub codex_command: Option<String>,
#[serde(rename = "CODEX_REASONING_EFFORT")]
pub codex_reasoning_effort: Option<String>,
#[serde(rename = "CODEX_ENABLE_SKILLS")]
pub codex_enable_skills: Option<String>,
#[serde(rename = "CODEX_SKIP_GIT_CHECK")]
pub codex_skip_git_check: Option<String>,
#[serde(rename = "CHATGPT_CODEX_REASONING_EFFORT")]
pub chatgpt_codex_reasoning_effort: Option<String>,
#[serde(rename = "CLAUDE_THINKING_TYPE")]
pub claude_thinking_type: Option<String>,
#[serde(rename = "CLAUDE_THINKING_EFFORT")]
pub claude_thinking_effort: Option<String>,
#[serde(rename = "CLAUDE_THINKING_BUDGET")]
pub claude_thinking_budget: Option<i32>,
#[serde(rename = "GEMINI3_THINKING_LEVEL")]
pub gemini3_thinking_level: Option<String>,
#[serde(rename = "GEMINI25_THINKING_BUDGET")]
pub gemini25_thinking_budget: Option<i32>,
// === Security Settings ===
#[serde(rename = "SECURITY_PROMPT_ENABLED")]
pub security_prompt_enabled: Option<bool>,
#[serde(rename = "SECURITY_PROMPT_THRESHOLD")]
pub security_prompt_threshold: Option<f64>,
#[serde(rename = "SECURITY_PROMPT_CLASSIFIER_ENABLED")]
pub security_prompt_classifier_enabled: Option<bool>,
#[serde(rename = "SECURITY_PROMPT_CLASSIFIER_MODEL")]
pub security_prompt_classifier_model: Option<String>,
#[serde(rename = "SECURITY_PROMPT_CLASSIFIER_ENDPOINT")]
pub security_prompt_classifier_endpoint: Option<String>,
#[serde(rename = "SECURITY_COMMAND_CLASSIFIER_ENABLED")]
pub security_command_classifier_enabled: Option<bool>,
// === Provider Settings ===
#[serde(rename = "OPENAI_HOST")]
pub openai_host: Option<String>,
#[serde(rename = "OPENAI_BASE_URL")]
pub openai_base_url: Option<String>,
#[serde(rename = "OPENAI_BASE_PATH")]
pub openai_base_path: Option<String>,
#[serde(rename = "OPENAI_ORGANIZATION")]
pub openai_organization: Option<String>,
#[serde(rename = "OPENAI_PROJECT")]
pub openai_project: Option<String>,
#[serde(rename = "OPENAI_TIMEOUT")]
pub openai_timeout: Option<u64>,
#[serde(rename = "ANTHROPIC_HOST")]
pub anthropic_host: Option<String>,
#[serde(rename = "OLLAMA_HOST")]
pub ollama_host: Option<String>,
#[serde(rename = "OLLAMA_TIMEOUT")]
pub ollama_timeout: Option<u64>,
#[serde(rename = "OLLAMA_STREAM_TIMEOUT")]
pub ollama_stream_timeout: Option<u64>,
#[serde(rename = "OLLAMA_STREAM_USAGE")]
pub ollama_stream_usage: Option<bool>,
#[serde(rename = "DATABRICKS_HOST")]
pub databricks_host: Option<String>,
#[serde(rename = "DATABRICKS_MAX_RETRIES")]
pub databricks_max_retries: Option<String>,
#[serde(rename = "DATABRICKS_INITIAL_RETRY_INTERVAL_MS")]
pub databricks_initial_retry_interval_ms: Option<String>,
#[serde(rename = "DATABRICKS_BACKOFF_MULTIPLIER")]
pub databricks_backoff_multiplier: Option<String>,
#[serde(rename = "DATABRICKS_MAX_RETRY_INTERVAL_MS")]
pub databricks_max_retry_interval_ms: Option<String>,
#[serde(rename = "AZURE_OPENAI_ENDPOINT")]
pub azure_openai_endpoint: Option<String>,
#[serde(rename = "AZURE_OPENAI_DEPLOYMENT_NAME")]
pub azure_openai_deployment_name: Option<String>,
#[serde(rename = "AZURE_OPENAI_API_VERSION")]
pub azure_openai_api_version: Option<String>,
#[serde(rename = "GOOGLE_HOST")]
pub google_host: Option<String>,
#[serde(rename = "GCP_PROJECT_ID")]
pub gcp_project_id: Option<String>,
#[serde(rename = "GCP_LOCATION")]
pub gcp_location: Option<String>,
#[serde(rename = "GCP_MAX_RETRIES")]
pub gcp_max_retries: Option<String>,
#[serde(rename = "GCP_INITIAL_RETRY_INTERVAL_MS")]
pub gcp_initial_retry_interval_ms: Option<String>,
#[serde(rename = "GCP_BACKOFF_MULTIPLIER")]
pub gcp_backoff_multiplier: Option<String>,
#[serde(rename = "GCP_MAX_RETRY_INTERVAL_MS")]
pub gcp_max_retry_interval_ms: Option<String>,
#[serde(rename = "AWS_REGION")]
pub aws_region: Option<String>,
#[serde(rename = "AWS_PROFILE")]
pub aws_profile: Option<String>,
#[serde(rename = "BEDROCK_MAX_RETRIES")]
pub bedrock_max_retries: Option<u64>,
#[serde(rename = "BEDROCK_INITIAL_RETRY_INTERVAL_MS")]
pub bedrock_initial_retry_interval_ms: Option<u64>,
#[serde(rename = "BEDROCK_BACKOFF_MULTIPLIER")]
pub bedrock_backoff_multiplier: Option<f64>,
#[serde(rename = "BEDROCK_MAX_RETRY_INTERVAL_MS")]
pub bedrock_max_retry_interval_ms: Option<u64>,
#[serde(rename = "BEDROCK_ENABLE_CACHING")]
pub bedrock_enable_caching: Option<bool>,
#[serde(rename = "SAGEMAKER_ENDPOINT_NAME")]
pub sagemaker_endpoint_name: Option<String>,
#[serde(rename = "LITELLM_HOST")]
pub litellm_host: Option<String>,
#[serde(rename = "LITELLM_BASE_PATH")]
pub litellm_base_path: Option<String>,
#[serde(rename = "LITELLM_TIMEOUT")]
pub litellm_timeout: Option<u64>,
#[serde(rename = "SNOWFLAKE_HOST")]
pub snowflake_host: Option<String>,
#[serde(rename = "GITHUB_COPILOT_HOST")]
pub github_copilot_host: Option<String>,
#[serde(rename = "GITHUB_COPILOT_CLIENT_ID")]
pub github_copilot_client_id: Option<String>,
#[serde(rename = "GITHUB_COPILOT_TOKEN_URL")]
pub github_copilot_token_url: Option<String>,
#[serde(rename = "XAI_HOST")]
pub xai_host: Option<String>,
#[serde(rename = "OPENROUTER_HOST")]
pub openrouter_host: Option<String>,
#[serde(rename = "VENICE_HOST")]
pub venice_host: Option<String>,
#[serde(rename = "VENICE_BASE_PATH")]
pub venice_base_path: Option<String>,
#[serde(rename = "VENICE_MODELS_PATH")]
pub venice_models_path: Option<String>,
#[serde(rename = "TETRATE_HOST")]
pub tetrate_host: Option<String>,
#[serde(rename = "AVIAN_HOST")]
pub avian_host: Option<String>,
// === Observability Settings (lowercase keys) ===
pub otel_exporter_otlp_endpoint: Option<String>,
pub otel_exporter_otlp_timeout: Option<u64>,
// === Tunnel Settings (lowercase keys) ===
pub tunnel_auto_start: Option<bool>,
// === Structured Config (lowercase keys) ===
pub extensions: Option<HashMap<String, ExtensionEntry>>,
pub slash_commands: Option<Vec<SlashCommandMapping>>,
pub experiments: Option<HashMap<String, bool>>,
}
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"
);
}
}
}
+2 -2
View File
@@ -22,7 +22,7 @@ const TOOLCALL_SUMMARIZATION_BATCH_SIZE: usize = 10;
fn tool_pair_summarization_enabled() -> bool {
Config::global()
.get_param::<bool>("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::<f64>("GOOSE_AUTO_COMPACT_THRESHOLD")
.get_goose_auto_compact_threshold()
.unwrap_or(DEFAULT_COMPACTION_THRESHOLD)
});
+2 -2
View File
@@ -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::<String>("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::<String>("OPENAI_HOST") {
} else if let Ok(host) = config.get_openai_host() {
(host, vec![], def.endpoint_path.to_string())
} else {
(
+1 -1
View File
@@ -14,7 +14,7 @@ pub fn get_context_filenames() -> Vec<String> {
use crate::config::Config;
Config::global()
.get_param::<Vec<String>>("CONTEXT_FILE_NAMES")
.get_context_file_names()
.unwrap_or_else(|_| {
vec![
GOOSE_HINTS_FILENAME.to_string(),
+2 -2
View File
@@ -88,7 +88,7 @@ impl ModelConfig {
None
}
} else {
match crate::config::Config::global().get_param::<usize>("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<Option<i32>, ConfigError> {
match crate::config::Config::global().get_param::<i32>("GOOSE_MAX_TOKENS") {
match crate::config::Config::global().get_goose_max_tokens() {
Ok(tokens) => {
if tokens <= 0 {
return Err(ConfigError::InvalidRange(
+2 -2
View File
@@ -83,12 +83,12 @@ pub fn signal_exporter(signal: &str) -> Option<ExporterType> {
/// 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::<String>("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::<u64>("otel_exporter_otlp_timeout") {
if let Ok(timeout) = config.get_otel_exporter_otlp_timeout() {
env::set_var("OTEL_EXPORTER_OTLP_TIMEOUT", timeout.to_string());
}
}
+8 -11
View File
@@ -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<AtomicBool> = 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<bool> {
}
let config = Config::global();
config.get_param::<bool>(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::<String>("GOOSE_PROVIDER") {
if let Ok(provider) = config.get_goose_provider() {
insert(&mut props, "provider", provider);
}
if let Ok(model) = config.get_param::<String>("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::<String>("GOOSE_PROVIDER") {
if let Ok(provider) = config.get_goose_provider() {
insert(&mut props, "provider", provider);
}
if let Ok(model) = config.get_param::<String>("GOOSE_MODEL") {
if let Ok(model) = config.get_goose_model() {
insert(&mut props, "model", model);
}
if let Ok(mode) = config.get_param::<String>("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::<i64>("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);
}
+2 -2
View File
@@ -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::<String>("ANTHROPIC_HOST")
.get_anthropic_host()
.unwrap_or_else(|_| "https://api.anthropic.com".to_string()),
);
+3 -3
View File
@@ -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::<String>("GOOSE_CLIENT_CERT_PATH").ok();
let client_key_path = config.get_param::<String>("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::<String>("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;
}
+1 -1
View File
@@ -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))?;
+3 -3
View File
@@ -75,10 +75,10 @@ impl ProviderDef for AzureProvider {
) -> BoxFuture<'static, Result<Self::Provider>> {
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
+7 -9
View File
@@ -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::<String>("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::<String>("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::<usize>("BEDROCK_MAX_RETRIES")
.get_bedrock_max_retries()
.unwrap_or(BEDROCK_DEFAULT_MAX_RETRIES);
let initial_interval_ms = config
.get_param::<u64>("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::<f64>("BEDROCK_BACKOFF_MULTIPLIER")
.get_bedrock_backoff_multiplier()
.unwrap_or(BEDROCK_DEFAULT_BACKOFF_MULTIPLIER);
let max_interval_ms = config
.get_param::<u64>("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::<bool>("BEDROCK_ENABLE_CACHING")
.unwrap_or(false);
let enabled = config.get_bedrock_enable_caching().unwrap_or(false);
enabled && self.model.model_name.contains("anthropic.claude")
}
+6 -6
View File
@@ -145,7 +145,7 @@ impl DatabricksProvider {
pub async fn from_env(model: ModelConfig) -> Result<Self> {
let config = crate::config::Config::global();
let mut host: Result<String, ConfigError> = config.get_param("DATABRICKS_HOST");
let mut host: Result<String, ConfigError> = 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::<usize>().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::<u64>().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::<f64>().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::<u64>().ok())
.unwrap_or(DEFAULT_MAX_RETRY_INTERVAL_MS);
@@ -259,7 +259,7 @@ impl DatabricksProvider {
fn resolve_instance_id() -> Option<String> {
let enabled = crate::config::Config::global()
.get_param::<bool>("GOOSE_DATABRICKS_CLIENT_REQUEST_ID")
.get_goose_databricks_client_request_id()
.unwrap_or(false);
if enabled {
Some(get_instance_id().to_string())
+6 -6
View File
@@ -166,7 +166,7 @@ impl GcpVertexAIProvider {
/// * `model` - Configuration for the model to be used
pub async fn from_env(model: ModelConfig) -> Result<Self> {
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::<usize>().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::<u64>().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::<f64>().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::<u64>().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<String> {
Ok(config
.get_param("GCP_LOCATION")
.get_gcp_location()
.ok()
.filter(|location: &String| !location.trim().is_empty())
.unwrap_or_else(|| GcpLocation::Iowa.to_string()))
+4 -4
View File
@@ -182,7 +182,7 @@ impl GithubCopilotProvider {
let config = Config::global();
let host = normalize_host(
&config
.get_param::<String>("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::<String>("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<String> = config.get_param("GITHUB_COPILOT_TOKEN_URL").ok();
let copilot_token_url: Option<String> = 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))
+2 -2
View File
@@ -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::<String>("GOOGLE_HOST")
.get_google_host()
.unwrap_or_else(|_| GOOGLE_API_HOST.to_string()),
);
+3 -3
View File
@@ -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<HashMap<String, String>> = 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
+9 -9
View File
@@ -56,7 +56,7 @@ pub struct OllamaProvider {
}
fn resolve_ollama_num_ctx(model_config: &ModelConfig) -> Option<usize> {
let config = crate::config::Config::global();
let input_limit = match config.get_param::<usize>("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<usize> {
fn resolve_ollama_stream_usage() -> bool {
let config = crate::config::Config::global();
match config.get_param::<bool>("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::<String>("OLLAMA_HOST").is_ok()
config.get_ollama_host().is_ok()
}
impl OllamaProvider {
pub async fn from_env(model: ModelConfig) -> Result<Self> {
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::<String>("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::<u64>("OLLAMA_STREAM_TIMEOUT") {
if let Ok(val) = config.get_ollama_stream_timeout() {
if val > 0 {
return val;
}
}
if let Ok(val) = config.get_param::<u64>("GOOSE_STREAM_TIMEOUT") {
if let Ok(val) = config.get_goose_stream_timeout() {
if val > 0 {
return val;
}
}
match config.get_param::<u64>("OLLAMA_TIMEOUT") {
match config.get_ollama_timeout() {
Ok(val) if val > 0 => val,
_ => OLLAMA_DEFAULT_CHUNK_TIMEOUT_SECS,
}
+41 -21
View File
@@ -156,7 +156,7 @@ impl OpenAiProvider {
from_base_url: false,
}
} else if let Some(raw_url) = config
.get_param::<String>("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<String> = config.get_param("OPENAI_ORGANIZATION").ok();
let project: Option<String> = config.get_param("OPENAI_PROJECT").ok();
let timeout_secs: u64 = config.get_param("OPENAI_TIMEOUT").unwrap_or(600);
let organization: Option<String> = config.get_openai_organization().ok();
let project: Option<String> = 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::<String>("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<InventoryIdentityInput> {
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::<String>("OPENAI_HOST")
.unwrap_or_else(|_| "https://api.openai.com".to_string()),
)
.with_public(
"base_path",
config
.get_param::<String>("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::<String>("OPENAI_ORGANIZATION") {
if let Ok(organization) = config.get_openai_organization() {
identity = identity.with_public("organization", organization);
}
if let Ok(project) = config.get_param::<String>("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") {
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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")
})?;
+1 -1
View File
@@ -60,7 +60,7 @@ pub struct SnowflakeProvider {
impl SnowflakeProvider {
pub async fn from_env(model: ModelConfig) -> Result<Self> {
let config = crate::config::Config::global();
let mut host: Result<String, ConfigError> = config.get_param("SNOWFLAKE_HOST");
let mut host: Result<String, ConfigError> = config.get_snowflake_host();
if host.is_err() {
host = config.get_secret("SNOWFLAKE_HOST")
}
+1 -1
View File
@@ -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);
+3 -3
View File
@@ -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);
+1 -1
View File
@@ -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))?;
+5 -7
View File
@@ -61,20 +61,18 @@ impl SecurityManager {
pub fn is_prompt_injection_detection_enabled(&self) -> bool {
let config = Config::global();
config
.get_param::<bool>("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::<bool>("SECURITY_PROMPT_CLASSIFIER_ENABLED")
.get_security_prompt_classifier_enabled()
.unwrap_or(false);
let command_enabled = config
.get_param::<bool>("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::<bool>("SECURITY_COMMAND_CLASSIFIER_ENABLED")
.get_security_command_classifier_enabled()
.unwrap_or(false);
let prompt_classifier_enabled = config
.get_param::<bool>("SECURITY_PROMPT_CLASSIFIER_ENABLED")
.get_security_prompt_classifier_enabled()
.unwrap_or(false);
tracing::info!(
+1 -1
View File
@@ -114,7 +114,7 @@ impl PromptInjectionScanner {
pub fn get_threshold_from_config(&self) -> f32 {
Config::global()
.get_param::<f64>("SECURITY_PROMPT_THRESHOLD")
.get_security_prompt_threshold()
.unwrap_or(0.8) as f32
}
+2 -1
View File
@@ -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,