Stringly typed config (#5463)

This commit is contained in:
Jack Amadeo
2025-10-30 15:13:32 -04:00
committed by GitHub
parent 1714990d42
commit 2970b5fa34
30 changed files with 243 additions and 275 deletions
+2 -2
View File
@@ -101,11 +101,11 @@ impl GooseAcpAgent {
let config = Config::global();
let provider_name: String = config
.get_param("GOOSE_PROVIDER")
.get_goose_provider()
.map_err(|e| anyhow::anyhow!("No provider configured: {}", e))?;
let model_name: String = config
.get_param("GOOSE_MODEL")
.get_goose_model()
.map_err(|e| anyhow::anyhow!("No model configured: {}", e))?;
let model_config = goose::model::ModelConfig {
+42 -56
View File
@@ -14,7 +14,8 @@ use goose::config::paths::Paths;
use goose::config::permission::PermissionLevel;
use goose::config::signup_tetrate::TetrateAuth;
use goose::config::{
configure_tetrate, Config, ConfigError, ExperimentManager, ExtensionEntry, PermissionManager,
configure_tetrate, Config, ConfigError, ExperimentManager, ExtensionEntry, GooseMode,
PermissionManager,
};
use goose::conversation::message::Message;
use goose::model::ModelConfig;
@@ -421,7 +422,7 @@ fn select_model_from_list(
}
fn try_store_secret(config: &Config, key_name: &str, value: String) -> anyhow::Result<bool> {
match config.set_secret(key_name, Value::String(value)) {
match config.set_secret(key_name, &value) {
Ok(_) => Ok(true),
Err(e) => {
cliclack::outro(style(format!(
@@ -450,7 +451,7 @@ pub async fn configure_provider_dialog() -> anyhow::Result<bool> {
.collect();
// Get current default provider if it exists
let current_provider: Option<String> = config.get_param("GOOSE_PROVIDER").ok();
let current_provider: Option<String> = config.get_goose_provider().ok();
let default_provider = current_provider.unwrap_or_default();
// Select provider
@@ -487,7 +488,7 @@ pub async fn configure_provider_dialog() -> anyhow::Result<bool> {
return Ok(false);
}
} else {
config.set_param(&key.name, Value::String(env_value))?;
config.set_param(&key.name, &env_value)?;
}
let _ = cliclack::log::info(format!("Saved {} to {}", key.name, config.path()));
}
@@ -529,7 +530,7 @@ pub async fn configure_provider_dialog() -> anyhow::Result<bool> {
return Ok(false);
}
} else {
config.set_param(&key.name, Value::String(value))?;
config.set_param(&key.name, &value)?;
}
}
}
@@ -558,9 +559,9 @@ pub async fn configure_provider_dialog() -> anyhow::Result<bool> {
};
if key.secret {
config.set_secret(&key.name, Value::String(value))?;
config.set_secret(&key.name, &value)?;
} else {
config.set_param(&key.name, Value::String(value))?;
config.set_param(&key.name, &value)?;
}
}
}
@@ -648,9 +649,8 @@ pub async fn configure_provider_dialog() -> anyhow::Result<bool> {
match result {
Ok((_message, _usage)) => {
// Update config with new values only if the test succeeds
config.set_param("GOOSE_PROVIDER", Value::String(provider_name.to_string()))?;
config.set_param("GOOSE_MODEL", Value::String(model.clone()))?;
config.set_goose_provider(provider_name)?;
config.set_goose_model(&model)?;
print_config_file_saved()?;
Ok(true)
}
@@ -877,7 +877,7 @@ pub fn configure_extensions_dialog() -> anyhow::Result<()> {
// Try to store in keychain
let keychain_key = key.to_string();
match config.set_secret(&keychain_key, Value::String(value.clone())) {
match config.set_secret(&keychain_key, &value) {
Ok(_) => {
// Successfully stored in keychain, add to env_keys
env_keys.push(keychain_key);
@@ -973,7 +973,7 @@ pub fn configure_extensions_dialog() -> anyhow::Result<()> {
// Try to store in keychain
let keychain_key = key.to_string();
match config.set_secret(&keychain_key, Value::String(value.clone())) {
match config.set_secret(&keychain_key, &value) {
Ok(_) => {
// Successfully stored in keychain, add to env_keys
env_keys.push(keychain_key);
@@ -1093,7 +1093,7 @@ pub fn configure_extensions_dialog() -> anyhow::Result<()> {
// Try to store in keychain
let keychain_key = key.to_string();
match config.set_secret(&keychain_key, Value::String(value.clone())) {
match config.set_secret(&keychain_key, &Value::String(value.clone())) {
Ok(_) => {
// Successfully stored in keychain, add to env_keys
env_keys.push(keychain_key);
@@ -1273,46 +1273,35 @@ pub fn configure_goose_mode_dialog() -> anyhow::Result<()> {
let mode = cliclack::select("Which goose mode would you like to configure?")
.item(
"auto",
GooseMode::Auto,
"Auto Mode",
"Full file modification, extension usage, edit, create and delete files freely"
)
.item(
"approve",
GooseMode::Approve,
"Approve Mode",
"All tools, extensions and file modifications will require human approval"
)
.item(
"smart_approve",
GooseMode::SmartApprove,
"Smart Approve Mode",
"Editing, creating, deleting files and using extensions will require human approval"
)
.item(
"chat",
GooseMode::Chat,
"Chat Mode",
"Engage with the selected provider without using tools, extensions, or file modification"
)
.interact()?;
match mode {
"auto" => {
config.set_param("GOOSE_MODE", Value::String("auto".to_string()))?;
cliclack::outro("Set to Auto Mode - full file modification enabled")?;
}
"approve" => {
config.set_param("GOOSE_MODE", Value::String("approve".to_string()))?;
cliclack::outro("Set to Approve Mode - all tools and modifications require approval")?;
}
"smart_approve" => {
config.set_param("GOOSE_MODE", Value::String("smart_approve".to_string()))?;
cliclack::outro("Set to Smart Approve Mode - modifications require approval")?;
}
"chat" => {
config.set_param("GOOSE_MODE", Value::String("chat".to_string()))?;
cliclack::outro("Set to Chat Mode - no tools or modifications enabled")?;
}
_ => unreachable!(),
config.set_goose_mode(mode)?;
let msg = match mode {
GooseMode::Auto => "Set to Auto Mode - full file modification enabled",
GooseMode::Approve => "Set to Approve Mode - all tools and modifications require approval",
GooseMode::SmartApprove => "Set to Smart Approve Mode - modifications require approval",
GooseMode::Chat => "Set to Chat Mode - no tools or modifications enabled",
};
cliclack::outro(msg)?;
Ok(())
}
@@ -1321,28 +1310,25 @@ pub fn configure_goose_router_strategy_dialog() -> anyhow::Result<()> {
let enable_router = cliclack::select("Would you like to enable smart tool routing?")
.item(
"true",
true,
"Enable Router",
"Use LLM-based intelligence to select tools",
)
.item(
"false",
false,
"Disable Router",
"Use the default tool selection strategy",
)
.interact()?;
match enable_router {
"true" => {
config.set_param("GOOSE_ENABLE_ROUTER", Value::String("true".to_string()))?;
cliclack::outro("Router enabled - using LLM-based intelligence for tool selection")?;
}
"false" => {
config.set_param("GOOSE_ENABLE_ROUTER", Value::String("false".to_string()))?;
cliclack::outro("Router disabled - using default tool selection")?;
}
_ => unreachable!(),
config.set_param("GOOSE_ENABLE_ROUTER", enable_router)?;
let msg = if enable_router {
"Router enabled - using LLM-based intelligence for tool selection"
} else {
"Router disabled - using default tool selection"
};
cliclack::outro(msg)?;
Ok(())
}
@@ -1360,15 +1346,15 @@ pub fn configure_tool_output_dialog() -> anyhow::Result<()> {
match tool_log_level {
"high" => {
config.set_param("GOOSE_CLI_MIN_PRIORITY", Value::from(0.8))?;
config.set_param("GOOSE_CLI_MIN_PRIORITY", 0.8)?;
cliclack::outro("Showing tool output of high importance only.")?;
}
"medium" => {
config.set_param("GOOSE_CLI_MIN_PRIORITY", Value::from(0.2))?;
config.set_param("GOOSE_CLI_MIN_PRIORITY", 0.2)?;
cliclack::outro("Showing tool output of medium importance.")?;
}
"all" => {
config.set_param("GOOSE_CLI_MIN_PRIORITY", Value::from(0.0))?;
config.set_param("GOOSE_CLI_MIN_PRIORITY", 0.0)?;
cliclack::outro("Showing all tool output.")?;
}
_ => unreachable!(),
@@ -1441,11 +1427,11 @@ pub async fn configure_tool_permissions_dialog() -> anyhow::Result<()> {
let config = Config::global();
let provider_name: String = config
.get_param("GOOSE_PROVIDER")
.get_goose_provider()
.expect("No provider configured. Please set model provider first");
let model: String = config
.get_param("GOOSE_MODEL")
.get_goose_model()
.expect("No model configured. Please set model first");
let model_config = ModelConfig::new(&model)?;
@@ -1591,7 +1577,7 @@ fn configure_recipe_dialog() -> anyhow::Result<()> {
if input_value.clone().trim().is_empty() {
config.delete(key_name)?;
} else {
config.set_param(key_name, Value::String(input_value))?;
config.set_param(key_name, &input_value)?;
}
Ok(())
}
@@ -1618,7 +1604,7 @@ pub fn configure_max_turns_dialog() -> anyhow::Result<()> {
.interact()?;
let max_turns: u32 = max_turns_input.parse()?;
config.set_param("GOOSE_MAX_TURNS", Value::from(max_turns))?;
config.set_param("GOOSE_MAX_TURNS", max_turns)?;
cliclack::outro(format!(
"Set maximum turns to {} - goose will ask for input after {} consecutive actions",
@@ -1651,7 +1637,7 @@ pub async fn handle_openrouter_auth() -> anyhow::Result<()> {
// Test configuration - get the model that was configured
println!("\nTesting configuration...");
let configured_model: String = config.get_param("GOOSE_MODEL")?;
let configured_model: String = config.get_goose_model()?;
let model_config = match goose::model::ModelConfig::new(&configured_model) {
Ok(config) => config,
Err(e) => {
@@ -1729,7 +1715,7 @@ pub async fn handle_tetrate_auth() -> anyhow::Result<()> {
// Test configuration
println!("\nTesting configuration...");
let configured_model: String = config.get_param("GOOSE_MODEL")?;
let configured_model: String = config.get_goose_model()?;
let model_config = match goose::model::ModelConfig::new(&configured_model) {
Ok(config) => config,
Err(e) => {
+2 -2
View File
@@ -139,7 +139,7 @@ pub async fn handle_web(
let config = goose::config::Config::global();
let provider_name: String = match config.get_param("GOOSE_PROVIDER") {
let provider_name: String = match config.get_goose_provider() {
Ok(p) => p,
Err(_) => {
eprintln!("No provider configured. Run 'goose configure' first");
@@ -147,7 +147,7 @@ pub async fn handle_web(
}
};
let model: String = match config.get_param("GOOSE_MODEL") {
let model: String = match config.get_goose_model() {
Ok(m) => m,
Err(_) => {
eprintln!("No model configured. Run 'goose configure' first");
+1 -2
View File
@@ -11,7 +11,6 @@ use goose::recipe::build_recipe::{
};
use goose::recipe::validate_recipe::parse_and_validate_parameters;
use goose::recipe::Recipe;
use serde_json::Value;
fn create_user_prompt_callback() -> impl Fn(&str, &str) -> Result<String> {
|key: &str, description: &str| -> Result<String> {
@@ -98,7 +97,7 @@ pub fn collect_missing_secrets(requirements: &[SecretRequirement]) -> Result<()>
.unwrap_or_else(|_| String::new());
if !value.trim().is_empty() {
config.set_secret(&req.key, Value::String(value))?;
config.set_secret(&req.key, &value)?;
println!("✅ Secret stored securely for {}", req.extension_name);
} else {
println!("⏭️ Skipped {} for {}", req.key, req.extension_name);
+2 -2
View File
@@ -208,7 +208,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
.as_ref()
.and_then(|s| s.goose_provider.clone())
})
.or_else(|| config.get_param("GOOSE_PROVIDER").ok())
.or_else(|| config.get_goose_provider().ok())
.expect("No provider configured. Run 'goose configure' first");
let model_name = session_config
@@ -219,7 +219,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
.as_ref()
.and_then(|s| s.goose_model.clone())
})
.or_else(|| config.get_param("GOOSE_MODEL").ok())
.or_else(|| config.get_goose_model().ok())
.expect("No model configured. Run 'goose configure' first");
let temperature = session_config.settings.as_ref().and_then(|s| s.temperature);
+22 -29
View File
@@ -12,6 +12,7 @@ use crate::session::task_execution_display::{
};
use goose::conversation::Conversation;
use std::io::Write;
use std::str::FromStr;
pub use self::export::message_to_markdown;
pub use builder::{build_session, SessionBuilderConfig, SessionSettings};
@@ -28,7 +29,7 @@ use completion::GooseCompleter;
use goose::agents::extension::{Envs, ExtensionConfig};
use goose::agents::types::RetryConfig;
use goose::agents::{Agent, SessionConfig};
use goose::config::Config;
use goose::config::{Config, GooseMode};
use goose::providers::pricing::initialize_pricing_cache;
use goose::session::SessionManager;
use input::InputResult;
@@ -545,21 +546,18 @@ impl CliSession {
save_history(&mut editor);
let config = Config::global();
let mode = mode.to_lowercase();
// Check if mode is valid
if !["auto", "approve", "chat", "smart_approve"].contains(&mode.as_str()) {
output::render_error(&format!(
"Invalid mode '{}'. Mode must be one of: auto, approve, chat",
mode
));
continue;
}
config
.set_param("GOOSE_MODE", Value::String(mode.to_string()))
.unwrap();
output::goose_mode_message(&format!("Goose mode set to '{}'", mode));
let mode = match GooseMode::from_str(&mode.to_lowercase()) {
Ok(mode) => mode,
Err(_) => {
output::render_error(&format!(
"Invalid mode '{}'. Mode must be one of: auto, approve, chat, smart_approve",
mode
));
continue;
}
};
config.set_goose_mode(mode)?;
output::goose_mode_message(&format!("Goose mode set to '{:?}'", mode));
continue;
}
input::InputResult::Plan(options) => {
@@ -787,12 +785,9 @@ impl CliSession {
self.run_mode = RunMode::Normal;
// set goose mode: auto if that isn't already the case
let config = Config::global();
let curr_goose_mode =
config.get_param("GOOSE_MODE").unwrap_or("auto".to_string());
if curr_goose_mode != "auto" {
config
.set_param("GOOSE_MODE", Value::String("auto".to_string()))
.unwrap();
let curr_goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto);
if curr_goose_mode != GooseMode::Auto {
config.set_goose_mode(GooseMode::Auto).unwrap();
}
// clear the messages before acting on the plan
@@ -807,10 +802,8 @@ impl CliSession {
output::hide_thinking();
// Reset run & goose mode
if curr_goose_mode != "auto" {
config
.set_param("GOOSE_MODE", Value::String(curr_goose_mode.to_string()))
.unwrap();
if curr_goose_mode != GooseMode::Auto {
config.set_goose_mode(curr_goose_mode)?;
}
} else {
// add the plan response (assistant message) & carry the conversation forward
@@ -1324,7 +1317,7 @@ impl CliSession {
.unwrap_or(false);
let provider_name = config
.get_param::<String>("GOOSE_PROVIDER")
.get_goose_provider()
.unwrap_or_else(|_| "unknown".to_string());
// Do not get costing information if show cost is disabled
@@ -1488,7 +1481,7 @@ async fn get_reasoner() -> Result<Arc<dyn Provider>, anyhow::Error> {
} else {
println!("WARNING: GOOSE_PLANNER_PROVIDER not found. Using default provider...");
config
.get_param::<String>("GOOSE_PROVIDER")
.get_goose_provider()
.expect("No provider configured. Run 'goose configure' first")
};
@@ -1498,7 +1491,7 @@ async fn get_reasoner() -> Result<Arc<dyn Provider>, anyhow::Error> {
} else {
println!("WARNING: GOOSE_PLANNER_MODEL not found. Using default model...");
config
.get_param::<String>("GOOSE_MODEL")
.get_goose_model()
.expect("No model configured. Run 'goose configure' first")
};
+2 -2
View File
@@ -68,7 +68,7 @@ thread_local! {
pub fn set_theme(theme: Theme) {
let config = Config::global();
config
.set_param("GOOSE_CLI_THEME", Value::String(theme.as_config_string()))
.set_param("GOOSE_CLI_THEME", theme.as_config_string())
.expect("Failed to set theme");
CURRENT_THEME.with(|t| *t.borrow_mut() = theme);
@@ -79,7 +79,7 @@ pub fn set_theme(theme: Theme) {
Theme::Ansi => "ansi",
};
if let Err(e) = config.set_param("GOOSE_CLI_THEME", Value::String(theme_str.to_string())) {
if let Err(e) = config.set_param("GOOSE_CLI_THEME", theme_str) {
eprintln!("Failed to save theme setting to config: {}", e);
}
}
+10 -16
View File
@@ -12,7 +12,7 @@ use axum::{
use goose::config::PermissionManager;
use goose::agents::ExtensionConfig;
use goose::config::Config;
use goose::config::{Config, GooseMode};
use goose::model::ModelConfig;
use goose::prompt_template::render_global_file;
use goose::providers::{create, create_with_named_model};
@@ -211,15 +211,12 @@ async fn resume_agent(
let config = Config::global();
let provider_result = async {
let provider_name: String =
config
.get_param("GOOSE_PROVIDER")
.map_err(|_| ErrorResponse {
message: "Could not configure agent: missing provider".into(),
status: StatusCode::INTERNAL_SERVER_ERROR,
})?;
let provider_name: String = config.get_goose_provider().map_err(|_| ErrorResponse {
message: "Could not configure agent: missing provider".into(),
status: StatusCode::INTERNAL_SERVER_ERROR,
})?;
let model: String = config.get_param("GOOSE_MODEL").map_err(|_| ErrorResponse {
let model: String = config.get_goose_model().map_err(|_| ErrorResponse {
message: "Could not configure agent: missing model".into(),
status: StatusCode::INTERNAL_SERVER_ERROR,
})?;
@@ -348,7 +345,7 @@ async fn get_tools(
Query(query): Query<GetToolsQuery>,
) -> Result<Json<Vec<ToolInfo>>, StatusCode> {
let config = Config::global();
let goose_mode = config.get_param("GOOSE_MODE").unwrap_or("auto".to_string());
let goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto);
let agent = state.get_agent_for_route(query.session_id).await?;
let permission_manager = PermissionManager::default();
@@ -360,9 +357,9 @@ async fn get_tools(
let permission = permission_manager
.get_user_permission(&tool.name)
.or_else(|| {
if goose_mode == "smart_approve" {
if goose_mode == GooseMode::SmartApprove {
permission_manager.get_smart_approve_permission(&tool.name)
} else if goose_mode == "approve" {
} else if goose_mode == GooseMode::Approve {
Some(PermissionLevel::AskBefore)
} else {
None
@@ -406,10 +403,7 @@ async fn update_agent_provider(
.await?;
let config = Config::global();
let model = match payload
.model
.or_else(|| config.get_param("GOOSE_MODEL").ok())
{
let model = match payload.model.or_else(|| config.get_goose_model().ok()) {
Some(m) => m,
None => {
tracing::error!("No model specified");
+1 -1
View File
@@ -251,7 +251,7 @@ async fn transcribe_elevenlabs_handler(
// Migrate to secret storage
if let Err(e) = config.set(
"ELEVENLABS_API_KEY",
serde_json::Value::String(key.clone()),
&serde_json::Value::String(key.clone()),
true,
) {
tracing::error!("Failed to migrate ElevenLabs API key: {:?}", e);
@@ -101,7 +101,7 @@ pub async fn upsert_config(
Json(query): Json<UpsertConfigQuery>,
) -> Result<Json<Value>, StatusCode> {
let config = Config::global();
let result = config.set(&query.key, query.value, query.is_secret);
let result = config.set(&query.key, &query.value, query.is_secret);
match result {
Ok(_) => Ok(Json(Value::String(format!("Upserted key {}", query.key)))),
+10 -13
View File
@@ -29,7 +29,7 @@ use crate::agents::tool_route_manager::ToolRouteManager;
use crate::agents::tool_router_index_manager::ToolRouterIndexManager;
use crate::agents::types::SessionConfig;
use crate::agents::types::{FrontendTool, SharedProvider, ToolResultReceiver};
use crate::config::{get_enabled_extensions, Config};
use crate::config::{get_enabled_extensions, Config, GooseMode};
use crate::context_mgmt::DEFAULT_COMPACTION_THRESHOLD;
use crate::conversation::{debug_conversation_fix, fix_conversation, Conversation};
use crate::mcp_utils::ToolResult;
@@ -73,7 +73,7 @@ pub struct ReplyContext {
pub tools: Vec<Tool>,
pub toolshim_tools: Vec<Tool>,
pub system_prompt: String,
pub goose_mode: String,
pub goose_mode: GooseMode,
pub initial_messages: Vec<Message>,
pub config: &'static Config,
}
@@ -193,7 +193,7 @@ impl Agent {
// Add permission inspector (medium-high priority)
// Note: mode will be updated dynamically based on session config
tool_inspection_manager.add_inspector(Box::new(PermissionInspector::new(
"smart_approve".to_string(),
GooseMode::SmartApprove,
std::collections::HashSet::new(), // readonly tools - will be populated from extension manager
std::collections::HashSet::new(), // regular tools - will be populated from extension manager
)));
@@ -264,7 +264,7 @@ impl Agent {
// Update permission inspector mode to match the session mode
self.tool_inspection_manager
.update_permission_inspector_mode(goose_mode.clone())
.update_permission_inspector_mode(goose_mode)
.await;
Ok(ReplyContext {
@@ -1030,8 +1030,7 @@ impl Agent {
yield AgentEvent::Message(msg);
}
let mode = goose_mode.clone();
if mode.as_str() == "chat" {
if goose_mode == GooseMode::Chat {
// Skip all tool calls in chat mode
for request in remaining_requests {
let mut response = message_tool_response.lock().await;
@@ -1258,15 +1257,13 @@ impl Agent {
}))
}
fn determine_goose_mode(session: Option<&SessionConfig>, config: &Config) -> String {
fn determine_goose_mode(session: Option<&SessionConfig>, config: &Config) -> GooseMode {
let mode = session.and_then(|s| s.execution_mode.as_deref());
match mode {
Some("foreground") => "chat".to_string(),
Some("background") => "auto".to_string(),
_ => config
.get_param("GOOSE_MODE")
.unwrap_or_else(|_| "auto".to_string()),
Some("foreground") => GooseMode::Chat,
Some("background") => GooseMode::Auto,
_ => config.get_goose_mode().unwrap_or(GooseMode::Auto),
}
}
@@ -1525,7 +1522,7 @@ impl Agent {
// but it doesn't know and the plumbing looks complicated.
let config = Config::global();
let provider_name: String = config
.get_param("GOOSE_PROVIDER")
.get_goose_provider()
.expect("No provider configured. Run 'goose configure' first");
let settings = Settings {
+10 -9
View File
@@ -3,13 +3,16 @@ use chrono::DateTime;
use chrono::Utc;
use serde::Serialize;
use serde_json::Value;
use std::borrow::Cow;
use std::collections::HashMap;
use crate::agents::extension::ExtensionInfo;
use crate::agents::recipe_tools::dynamic_task_tools::should_enabled_subagents;
use crate::agents::router_tools::llm_search_tool_prompt;
use crate::{config::Config, prompt_template, utils::sanitize_unicode_tags};
use crate::{
config::{Config, GooseMode},
prompt_template,
utils::sanitize_unicode_tags,
};
const MAX_EXTENSIONS: usize = 5;
const MAX_TOOLS: usize = 50;
@@ -34,7 +37,7 @@ struct SystemPromptContext {
current_date_time: String,
#[serde(skip_serializing_if = "Option::is_none")]
extension_tool_limits: Option<(usize, usize)>,
goose_mode: String,
goose_mode: GooseMode,
is_autonomous: bool,
enable_subagents: bool,
max_extensions: usize,
@@ -106,9 +109,7 @@ impl<'a> SystemPromptBuilder<'a, PromptManager> {
.collect();
let config = Config::global();
let goose_mode = config
.get_param("GOOSE_MODE")
.unwrap_or_else(|_| Cow::from("auto"));
let goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto);
let extension_tool_limits = self
.extension_tool_count
@@ -119,8 +120,8 @@ impl<'a> SystemPromptBuilder<'a, PromptManager> {
tool_selection_strategy: self.router_enabled.then(llm_search_tool_prompt),
current_date_time: self.manager.current_date_timestamp.clone(),
extension_tool_limits,
goose_mode: goose_mode.to_string(),
is_autonomous: goose_mode == "auto",
goose_mode,
is_autonomous: goose_mode == GooseMode::Auto,
enable_subagents: should_enabled_subagents(self.model_name.as_str()),
max_extensions: MAX_EXTENSIONS,
max_tools: MAX_TOOLS,
@@ -137,7 +138,7 @@ impl<'a> SystemPromptBuilder<'a, PromptManager> {
});
let mut system_prompt_extras = self.manager.system_prompt_extras.clone();
if goose_mode == "chat" {
if goose_mode == GooseMode::Chat {
system_prompt_extras.push(
"Right now you are in the chat only mode, no access to any tool use and system."
.to_string(),
@@ -9,6 +9,7 @@ use crate::agents::subagent_execution_tool::{
task_types::{Task, TaskType},
};
use crate::agents::tool_execution::ToolCallResult;
use crate::config::GooseMode;
use crate::recipe::{Recipe, RecipeBuilder};
use anyhow::{anyhow, Result};
use rmcp::model::{Content, ErrorCode, ErrorData, Tool, ToolAnnotations};
@@ -93,7 +94,7 @@ pub struct TaskParameter {
pub fn should_enabled_subagents(model_name: &str) -> bool {
let config = crate::config::Config::global();
let is_autonomous = config.get_param("GOOSE_MODE").unwrap_or("auto".to_string()) == "auto";
let is_autonomous = config.get_goose_mode().unwrap_or(GooseMode::Auto) == GooseMode::Auto;
if !is_autonomous {
return false;
}
+40 -33
View File
@@ -1,8 +1,9 @@
use crate::config::paths::Paths;
use crate::config::GooseMode;
use fs2::FileExt;
use keyring::Entry;
use once_cell::sync::OnceCell;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::env;
@@ -143,6 +144,11 @@ macro_rules! declare_param {
self.get_param(stringify!($param_name))
}
}
paste::paste! {
pub fn [<set_ $param_name:lower>](&self, v: impl Into<$param_type>) -> Result<(), ConfigError> {
self.set_param(stringify!($param_name), &v.into())
}
}
};
}
@@ -551,7 +557,10 @@ impl Config {
}
// save a parameter in the appropriate location based on if it's secret or not
pub fn set(&self, key: &str, value: Value, is_secret: bool) -> Result<(), ConfigError> {
pub fn set<V>(&self, key: &str, value: &V, is_secret: bool) -> Result<(), ConfigError>
where
V: Serialize,
{
if is_secret {
self.set_secret(key, value)
} else {
@@ -606,17 +615,10 @@ impl Config {
/// Returns a ConfigError if:
/// - There is an error reading or writing the config file
/// - There is an error serializing the value
pub fn set_param(&self, key: &str, value: Value) -> Result<(), ConfigError> {
// Lock before reading to prevent race condition.
pub fn set_param<V: Serialize>(&self, key: &str, value: V) -> Result<(), ConfigError> {
let _guard = self.guard.lock().unwrap();
// Load current values with recovery if needed
let mut values = self.load_values()?;
// Modify values
values.insert(key.to_string(), value);
// Save all values using the atomic write approach
values.insert(key.to_string(), serde_json::to_value(&value)?);
self.save_values(values)
}
@@ -689,12 +691,15 @@ impl Config {
/// Returns a ConfigError if:
/// - There is an error accessing the keyring
/// - There is an error serializing the value
pub fn set_secret(&self, key: &str, value: Value) -> Result<(), ConfigError> {
pub fn set_secret<V>(&self, key: &str, value: &V) -> Result<(), ConfigError>
where
V: Serialize,
{
// Lock before reading to prevent race condition.
let _guard = self.guard.lock().unwrap();
let mut values = self.load_secrets()?;
values.insert(key.to_string(), value);
values.insert(key.to_string(), serde_json::to_value(value)?);
match &self.secrets {
SecretStorage::Keyring { service } => {
@@ -742,6 +747,9 @@ impl Config {
}
declare_param!(GOOSE_SEARCH_PATHS, Vec<String>);
declare_param!(GOOSE_MODE, GooseMode);
declare_param!(GOOSE_PROVIDER, String);
declare_param!(GOOSE_MODEL, String);
}
/// Load init-config.yaml from workspace root if it exists.
@@ -819,7 +827,7 @@ mod tests {
let config = Config::new(temp_file.path(), TEST_KEYRING_SERVICE)?;
// Set a simple string value
config.set_param("test_key", Value::String("test_value".to_string()))?;
config.set_param("test_key", "test_value")?;
// Test simple string retrieval
let value: String = config.get_param("test_key")?;
@@ -874,8 +882,8 @@ mod tests {
let temp_file = NamedTempFile::new().unwrap();
let config = Config::new(temp_file.path(), TEST_KEYRING_SERVICE)?;
config.set_param("key1", Value::String("value1".to_string()))?;
config.set_param("key2", Value::Number(42.into()))?;
config.set_param("key1", "value1")?;
config.set_param("key2", 42)?;
// Read the file directly to check YAML formatting
let content = std::fs::read_to_string(temp_file.path())?;
@@ -890,12 +898,11 @@ mod tests {
let temp_file = NamedTempFile::new().unwrap();
let config = Config::new(temp_file.path(), TEST_KEYRING_SERVICE)?;
config.set_param("key", Value::String("value".to_string()))?;
config.set_param("test_key", "test_value")?;
config.set_param("another_key", 42)?;
config.set_param("third_key", true)?;
let value: String = config.get_param("key")?;
assert_eq!(value, "value");
config.delete("key")?;
let _values = config.load_values()?;
let result: Result<String, ConfigError> = config.get_param("key");
assert!(matches!(result, Err(ConfigError::NotFound(_))));
@@ -909,7 +916,7 @@ mod tests {
let secrets_file = NamedTempFile::new().unwrap();
let config = Config::new_with_file_secrets(config_file.path(), secrets_file.path())?;
config.set_secret("key", Value::String("value".to_string()))?;
config.set_secret("key", &"value")?;
let value: String = config.get_secret("key")?;
assert_eq!(value, "value");
@@ -930,7 +937,7 @@ mod tests {
let config = Config::new(temp_file.path(), TEST_KEYRING_SERVICE)?;
// Test setting and getting a simple secret
config.set_secret("api_key", Value::String("secret123".to_string()))?;
config.set_secret("api_key", &Value::String("secret123".to_string()))?;
let value: String = config.get_secret("api_key")?;
assert_eq!(value, "secret123");
@@ -957,8 +964,8 @@ mod tests {
let config = Config::new(temp_file.path(), TEST_KEYRING_SERVICE)?;
// Set multiple secrets
config.set_secret("key1", Value::String("secret1".to_string()))?;
config.set_secret("key2", Value::String("secret2".to_string()))?;
config.set_secret("key1", &Value::String("secret1".to_string()))?;
config.set_secret("key2", &Value::String("secret2".to_string()))?;
// Verify both exist
let value1: String = config.get_secret("key1")?;
@@ -1056,7 +1063,7 @@ mod tests {
let config = Config::new(temp_file.path(), TEST_KEYRING_SERVICE)?;
// Create a valid config first
config.set_param("key1", Value::String("value1".to_string()))?;
config.set_param("key1", "value1")?;
// Verify the backup was created by the first write
let backup_paths = config.get_backup_paths();
@@ -1066,7 +1073,7 @@ mod tests {
}
// Make another write to ensure backup is created
config.set_param("key2", Value::Number(42.into()))?;
config.set_param("key2", 42)?;
// Check again
for (i, path) in backup_paths.iter().enumerate() {
@@ -1160,15 +1167,15 @@ mod tests {
let config = Config::new(config_path, TEST_KEYRING_SERVICE)?;
// First, create a config with some data
config.set_param("test_key_backup", Value::String("backup_value".to_string()))?;
config.set_param("another_key", Value::Number(42.into()))?;
config.set_param("test_key_backup", "backup_value")?;
config.set_param("another_key", 42)?;
// Verify the backup was created
let backup_paths = config.get_backup_paths();
let primary_backup = &backup_paths[0]; // .bak file
// Make sure we have a backup by doing another write
config.set_param("third_key", Value::Bool(true))?;
config.set_param("third_key", true)?;
assert!(primary_backup.exists(), "Backup should exist after writes");
// Now delete the main config file to simulate it being lost
@@ -1204,7 +1211,7 @@ mod tests {
let config = Config::new(temp_file.path(), TEST_KEYRING_SERVICE)?;
// Set initial values
config.set_param("key1", Value::String("value1".to_string()))?;
config.set_param("key1", "value1")?;
// Verify the config file exists and is valid
assert!(temp_file.path().exists());
@@ -1225,7 +1232,7 @@ mod tests {
// Create multiple versions to test rotation
for i in 1..=7 {
config.set_param("version", Value::Number(i.into()))?;
config.set_param("version", i)?;
}
let backup_paths = config.get_backup_paths();
@@ -1460,7 +1467,7 @@ mod tests {
let config = Config::new(temp_file.path(), TEST_KEYRING_SERVICE)?;
// Set value in config file
config.set_param("test_precedence", Value::String("file_value".to_string()))?;
config.set_param("test_precedence", "file_value")?;
// Verify file value is returned when no env var
let value: String = config.get_param("test_precedence")?;
@@ -97,7 +97,7 @@ pub fn create_custom_provider(
let api_key_name = generate_api_key_name(&id);
let config = Config::global();
config.set_secret(&api_key_name, serde_json::Value::String(api_key))?;
config.set_secret(&api_key_name, &api_key)?;
let model_infos: Vec<ModelInfo> = models
.into_iter()
@@ -147,10 +147,7 @@ pub fn update_custom_provider(
let config = Config::global();
if !api_key.is_empty() {
config.set_secret(
&existing_config.api_key_env,
serde_json::Value::String(api_key),
)?;
config.set_secret(&existing_config.api_key_env, &api_key)?;
}
if editable {
+1 -1
View File
@@ -35,7 +35,7 @@ impl ExperimentManager {
Self::refresh_experiments(&mut experiments);
experiments.insert(name.to_string(), enabled);
config.set_param("experiments", serde_json::to_value(experiments)?)?;
config.set_param("experiments", experiments)?;
Ok(())
}
+3 -9
View File
@@ -101,15 +101,9 @@ fn get_extensions_map() -> HashMap<String, ExtensionEntry> {
fn save_extensions_map(extensions: HashMap<String, ExtensionEntry>) {
let config = Config::global();
match serde_json::to_value(extensions) {
Ok(value) => {
if let Err(e) = config.set_param(EXTENSIONS_CONFIG_KEY, value) {
tracing::debug!("Failed to save extensions config: {}", e);
}
}
Err(e) => {
tracing::debug!("Failed to serialize extensions: {}", e);
}
if let Err(e) = config.set_param(EXTENSIONS_CONFIG_KEY, &extensions) {
// TODO(jack) why is this just a debug statement?
tracing::debug!("Failed to save extensions config: {}", e);
}
}
+26
View File
@@ -0,0 +1,26 @@
use std::str::FromStr;
use serde::{Deserialize, Serialize};
#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GooseMode {
Auto,
Approve,
SmartApprove,
Chat,
}
impl FromStr for GooseMode {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"auto" => Ok(GooseMode::Auto),
"approve" => Ok(GooseMode::Approve),
"smart_approve" => Ok(GooseMode::SmartApprove),
"chat" => Ok(GooseMode::Chat),
_ => Err(format!("invalid mode: {}", s)),
}
}
}
+2
View File
@@ -2,6 +2,7 @@ pub mod base;
pub mod declarative_providers;
mod experiments;
pub mod extensions;
pub mod goose_mode;
pub mod paths;
pub mod permission;
pub mod search_path;
@@ -16,6 +17,7 @@ pub use extensions::{
get_all_extension_names, get_all_extensions, get_enabled_extensions, get_extension_by_name,
is_extension_enabled, remove_extension, set_extension, set_extension_enabled, ExtensionEntry,
};
pub use goose_mode::GooseMode;
pub use permission::PermissionManager;
pub use signup_openrouter::configure_openrouter;
pub use signup_tetrate::configure_tetrate;
@@ -162,14 +162,10 @@ impl PkceAuthFlow {
pub use self::PkceAuthFlow as OpenRouterAuth;
use crate::config::Config;
use serde_json::Value;
pub fn configure_openrouter(config: &Config, api_key: String) -> Result<()> {
config.set_secret("OPENROUTER_API_KEY", Value::String(api_key))?;
config.set_param("GOOSE_PROVIDER", Value::String("openrouter".to_string()))?;
config.set_param(
"GOOSE_MODEL",
Value::String(OPENROUTER_DEFAULT_MODEL.to_string()),
)?;
config.set_secret("OPENROUTER_API_KEY", &api_key)?;
config.set_goose_provider("openrouter")?;
config.set_goose_model(OPENROUTER_DEFAULT_MODEL)?;
Ok(())
}
@@ -163,14 +163,10 @@ impl PkceAuthFlow {
pub use self::PkceAuthFlow as TetrateAuth;
use crate::config::Config;
use serde_json::Value;
pub fn configure_tetrate(config: &Config, api_key: String) -> Result<()> {
config.set_secret("TETRATE_API_KEY", Value::String(api_key))?;
config.set_param("GOOSE_PROVIDER", Value::String("tetrate".to_string()))?;
config.set_param(
"GOOSE_MODEL",
Value::String(TETRATE_DEFAULT_MODEL.to_string()),
)?;
config.set_secret("TETRATE_API_KEY", &api_key)?;
config.set_goose_provider("tetrate")?;
config.set_goose_model(TETRATE_DEFAULT_MODEL)?;
Ok(())
}
@@ -76,12 +76,9 @@ fn test_configure_tetrate() {
config.get_secret::<String>("TETRATE_API_KEY").unwrap(),
test_key
);
assert_eq!(config.get_goose_provider().unwrap(), "tetrate");
assert_eq!(
config.get_param::<String>("GOOSE_PROVIDER").unwrap(),
"tetrate"
);
assert_eq!(
config.get_param::<String>("GOOSE_MODEL").unwrap(),
config.get_goose_model().unwrap(),
TETRATE_DEFAULT_MODEL.to_string()
);
}
+1 -2
View File
@@ -27,9 +27,8 @@ pub async fn save_credentials(
token_response,
};
let value = serde_json::to_value(&credentials)?;
let key = secret_key(name);
config.set_secret(&key, value)?;
config.set_secret(&key, &credentials)?;
Ok(())
}
@@ -1,6 +1,6 @@
use crate::agents::extension_manager_extension::MANAGE_EXTENSIONS_TOOL_NAME_COMPLETE;
use crate::config::permission::PermissionLevel;
use crate::config::PermissionManager;
use crate::config::{GooseMode, PermissionManager};
use crate::conversation::message::{Message, ToolRequest};
use crate::permission::permission_judge::PermissionCheckResult;
use crate::tool_inspection::{InspectionAction, InspectionResult, ToolInspector};
@@ -12,7 +12,7 @@ use tokio::sync::Mutex;
/// Permission Inspector that handles tool permission checking
pub struct PermissionInspector {
mode: Arc<Mutex<String>>,
mode: Arc<Mutex<GooseMode>>,
readonly_tools: HashSet<String>,
regular_tools: HashSet<String>,
pub permission_manager: Arc<Mutex<PermissionManager>>,
@@ -20,7 +20,7 @@ pub struct PermissionInspector {
impl PermissionInspector {
pub fn new(
mode: String,
mode: GooseMode,
readonly_tools: HashSet<String>,
regular_tools: HashSet<String>,
) -> Self {
@@ -33,7 +33,7 @@ impl PermissionInspector {
}
pub fn with_permission_manager(
mode: String,
mode: GooseMode,
readonly_tools: HashSet<String>,
regular_tools: HashSet<String>,
permission_manager: Arc<Mutex<PermissionManager>>,
@@ -47,7 +47,7 @@ impl PermissionInspector {
}
/// Update the mode of this permission inspector
pub async fn update_mode(&self, new_mode: String) {
pub async fn update_mode(&self, new_mode: GooseMode) {
let mut mode = self.mode.lock().await;
*mode = new_mode;
}
@@ -139,45 +139,42 @@ impl ToolInspector for PermissionInspector {
if let Ok(tool_call) = &request.tool_call {
let tool_name = &tool_call.name;
// Handle different modes
let action = if *mode == "chat" {
// In chat mode, all tools are skipped (handled elsewhere)
continue;
} else if *mode == "auto" {
// In auto mode, all tools are approved
InspectionAction::Allow
} else {
// Smart mode - check permissions
// 1. Check user-defined permission first
if let Some(level) = permission_manager.get_user_permission(tool_name) {
match level {
PermissionLevel::AlwaysAllow => InspectionAction::Allow,
PermissionLevel::NeverAllow => InspectionAction::Deny,
PermissionLevel::AskBefore => InspectionAction::RequireApproval(None),
let action = match *mode {
GooseMode::Chat => continue,
GooseMode::Auto => InspectionAction::Allow,
GooseMode::Approve | GooseMode::SmartApprove => {
// 1. Check user-defined permission first
if let Some(level) = permission_manager.get_user_permission(tool_name) {
match level {
PermissionLevel::AlwaysAllow => InspectionAction::Allow,
PermissionLevel::NeverAllow => InspectionAction::Deny,
PermissionLevel::AskBefore => {
InspectionAction::RequireApproval(None)
}
}
}
// 2. Check if it's a readonly or regular tool (both pre-approved)
else if self.readonly_tools.contains(tool_name.as_ref())
|| self.regular_tools.contains(tool_name.as_ref())
{
InspectionAction::Allow
}
// 4. Special case for extension management
else if tool_name == MANAGE_EXTENSIONS_TOOL_NAME_COMPLETE {
InspectionAction::RequireApproval(Some(
"Extension management requires approval for security".to_string(),
))
}
// 5. Default: require approval for unknown tools
else {
InspectionAction::RequireApproval(None)
}
}
// 2. Check if it's a readonly or regular tool (both pre-approved)
else if self.readonly_tools.contains(tool_name.as_ref())
|| self.regular_tools.contains(tool_name.as_ref())
{
InspectionAction::Allow
}
// 4. Special case for extension management
else if tool_name == MANAGE_EXTENSIONS_TOOL_NAME_COMPLETE {
InspectionAction::RequireApproval(Some(
"Extension management requires approval for security".to_string(),
))
}
// 5. Default: require approval for unknown tools
else {
InspectionAction::RequireApproval(None)
}
};
let reason = match &action {
InspectionAction::Allow => {
if *mode == "auto" {
if *mode == GooseMode::Auto {
"Auto mode - all tools approved".to_string()
} else if self.readonly_tools.contains(tool_name.as_ref()) {
"Tool marked as read-only".to_string()
+3 -17
View File
@@ -10,7 +10,7 @@ use tokio::process::Command;
use super::base::{ConfigKey, Provider, ProviderMetadata, ProviderUsage, Usage};
use super::errors::ProviderError;
use super::utils::RequestLog;
use crate::config::Config;
use crate::config::{Config, GooseMode};
use crate::conversation::message::{Message, MessageContent};
use crate::model::ModelConfig;
use rmcp::model::Tool;
@@ -338,10 +338,8 @@ impl ClaudeCodeProvider {
// Add permission mode based on GOOSE_MODE setting
let config = Config::global();
if let Ok(goose_mode) = config.get_param::<String>("GOOSE_MODE") {
if goose_mode.as_str() == "auto" {
cmd.arg("--permission-mode").arg("acceptEdits");
}
if let Ok(GooseMode::Auto) = config.get_goose_mode() {
cmd.arg("--permission-mode").arg("acceptEdits");
}
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
@@ -523,18 +521,6 @@ mod tests {
use super::ModelConfig;
use super::*;
#[test]
fn test_permission_mode_flag_construction() {
// Test that in auto mode, the --permission-mode acceptEdits flag is added
std::env::set_var("GOOSE_MODE", "auto");
let config = Config::global();
let goose_mode: String = config.get_param("GOOSE_MODE").unwrap();
assert_eq!(goose_mode, "auto");
std::env::remove_var("GOOSE_MODE");
}
#[tokio::test]
async fn test_claude_code_invalid_model_no_fallback() {
// Test that an invalid model is kept as-is (no fallback)
+2 -2
View File
@@ -235,7 +235,7 @@ impl GithubCopilotProvider {
.get_access_token()
.await
.context("unable to login into github")?;
config.set_secret("GITHUB_COPILOT_TOKEN", Value::String(token.clone()))?;
config.set_secret("GITHUB_COPILOT_TOKEN", &token)?;
token
}
_ => return Err(err.into()),
@@ -500,7 +500,7 @@ impl Provider for GithubCopilotProvider {
// Save the token
config
.set_secret("GITHUB_COPILOT_TOKEN", Value::String(token))
.set_secret("GITHUB_COPILOT_TOKEN", &token)
.map_err(|e| ProviderError::ExecutionError(format!("Failed to save token: {}", e)))?;
Ok(())
+7 -2
View File
@@ -6,6 +6,7 @@ use super::utils::{
get_model, handle_response_openai_compat, handle_status_openai_compat, RequestLog,
};
use crate::config::declarative_providers::DeclarativeProviderConfig;
use crate::config::GooseMode;
use crate::conversation::message::Message;
use crate::conversation::Conversation;
@@ -199,8 +200,12 @@ impl Provider for OllamaProvider {
tools: &[Tool],
) -> Result<(Message, ProviderUsage), ProviderError> {
let config = crate::config::Config::global();
let goose_mode = config.get_param("GOOSE_MODE").unwrap_or("auto".to_string());
let filtered_tools = if goose_mode == "chat" { &[] } else { tools };
let goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto);
let filtered_tools = if goose_mode == GooseMode::Chat {
&[]
} else {
tools
};
let payload = create_request(
&self.model,
+2 -2
View File
@@ -1104,7 +1104,7 @@ async fn run_scheduled_job_internal(
agent_provider = provider;
} else {
let global_config = Config::global();
let provider_name: String = match global_config.get_param("GOOSE_PROVIDER") {
let provider_name: String = match global_config.get_goose_provider() {
Ok(name) => name,
Err(_) => return Err(JobExecutionError {
job_id: job.id.clone(),
@@ -1114,7 +1114,7 @@ async fn run_scheduled_job_internal(
}),
};
let model_name: String =
match global_config.get_param("GOOSE_MODEL") {
match global_config.get_goose_model() {
Ok(name) => name,
Err(_) => return Err(JobExecutionError {
job_id: job.id.clone(),
+2 -1
View File
@@ -2,6 +2,7 @@ use anyhow::Result;
use async_trait::async_trait;
use std::collections::HashMap;
use crate::config::GooseMode;
use crate::conversation::message::{Message, ToolRequest};
use crate::permission::permission_inspector::PermissionInspector;
use crate::permission::permission_judge::PermissionCheckResult;
@@ -116,7 +117,7 @@ impl ToolInspectionManager {
}
/// Update the permission inspector's mode
pub async fn update_permission_inspector_mode(&self, mode: String) {
pub async fn update_permission_inspector_mode(&self, mode: GooseMode) {
for inspector in &self.inspectors {
if inspector.name() == "permission" {
// Downcast to PermissionInspector to access update_mode method
+2 -8
View File
@@ -301,16 +301,10 @@ mod tests {
// Set values in config
test_config
.set_param(
"otel_exporter_otlp_endpoint",
serde_json::Value::String("http://config:4318".to_string()),
)
.set_param("otel_exporter_otlp_endpoint", "http://config:4318")
.unwrap();
test_config
.set_param(
"otel_exporter_otlp_timeout",
serde_json::Value::Number(3000.into()),
)
.set_param("otel_exporter_otlp_timeout", 3000)
.unwrap();
// Test that from_config reads from the config file