diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 817f777db6d7..d76302e10c72 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -61,7 +61,7 @@ As you make changes to the rust code, you can try it out on the CLI, or also run cargo check # do your changes compile cargo test # do the tests pass with your changes cargo fmt # format your code -./scripts/clippy-lint # run the linter +./scripts/clippy-lint.sh # run the linter ``` ### Node diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index e0a05c023c7e..00669bb15334 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -246,6 +246,39 @@ pub async fn handle_configure() -> Result<(), Box> { } } +/// Helper function to handle OAuth configuration for a provider +async fn handle_oauth_configuration( + provider_name: &str, + key_name: &str, +) -> Result<(), Box> { + use goose::model::ModelConfig; + use goose::providers::create; + + let _ = cliclack::log::info(format!( + "Configuring {} using OAuth device code flow...", + key_name + )); + + // Create a temporary provider instance to handle OAuth + let temp_model = ModelConfig::new("temp")?; + match create(provider_name, temp_model) { + Ok(provider) => match provider.configure_oauth().await { + Ok(_) => { + let _ = cliclack::log::success("OAuth authentication completed successfully!"); + Ok(()) + } + Err(e) => { + let _ = cliclack::log::error(format!("Failed to authenticate: {}", e)); + Err(format!("OAuth authentication failed for {}: {}", key_name, e).into()) + } + }, + Err(e) => { + let _ = cliclack::log::error(format!("Failed to create provider for OAuth: {}", e)); + Err(format!("Failed to create provider for OAuth: {}", e).into()) + } + } +} + /// Dialog for configuring the AI provider and model pub async fn configure_provider_dialog() -> Result> { // Get global config instance @@ -313,13 +346,52 @@ pub async fn configure_provider_dialog() -> Result> { Ok(_) => { let _ = cliclack::log::info(format!("{} is already configured", key.name)); if cliclack::confirm("Would you like to update this value?").interact()? { - let new_value: String = if key.secret { - cliclack::password(format!("Enter new value for {}", key.name)) - .mask('▪') - .interact()? + // Check if this key uses OAuth flow + if key.oauth_flow { + handle_oauth_configuration(provider_name, &key.name).await?; } else { - let mut input = - cliclack::input(format!("Enter new value for {}", key.name)); + // Non-OAuth key, use manual entry + let value: String = if key.secret { + cliclack::password(format!("Enter new value for {}", key.name)) + .mask('▪') + .interact()? + } else { + let mut input = cliclack::input(format!( + "Enter new value for {}", + key.name + )); + if key.default.is_some() { + input = input.default_input(&key.default.clone().unwrap()); + } + input.interact()? + }; + + if key.secret { + config.set_secret(&key.name, Value::String(value))?; + } else { + config.set_param(&key.name, Value::String(value))?; + } + } + } + } + Err(_) => { + // Check if this key uses OAuth flow + if key.oauth_flow { + handle_oauth_configuration(provider_name, &key.name).await?; + } else { + // Non-OAuth key, use manual entry + let value: String = if key.secret { + cliclack::password(format!( + "Provider {} requires {}, please enter a value", + provider_meta.display_name, key.name + )) + .mask('▪') + .interact()? + } else { + let mut input = cliclack::input(format!( + "Provider {} requires {}, please enter a value", + provider_meta.display_name, key.name + )); if key.default.is_some() { input = input.default_input(&key.default.clone().unwrap()); } @@ -327,35 +399,10 @@ pub async fn configure_provider_dialog() -> Result> { }; if key.secret { - config.set_secret(&key.name, Value::String(new_value))?; + config.set_secret(&key.name, Value::String(value))?; } else { - config.set_param(&key.name, Value::String(new_value))?; - } - } - } - Err(_) => { - let value: String = if key.secret { - cliclack::password(format!( - "Provider {} requires {}, please enter a value", - provider_meta.display_name, key.name - )) - .mask('▪') - .interact()? - } else { - let mut input = cliclack::input(format!( - "Provider {} requires {}, please enter a value", - provider_meta.display_name, key.name - )); - if key.default.is_some() { - input = input.default_input(&key.default.clone().unwrap()); + config.set_param(&key.name, Value::String(value))?; } - input.interact()? - }; - - if key.secret { - config.set_secret(&key.name, Value::String(value))?; - } else { - config.set_param(&key.name, Value::String(value))?; } } } diff --git a/crates/goose/src/providers/base.rs b/crates/goose/src/providers/base.rs index 6366deeea571..48e22deabf17 100644 --- a/crates/goose/src/providers/base.rs +++ b/crates/goose/src/providers/base.rs @@ -162,21 +162,45 @@ impl ProviderMetadata { } } +/// Configuration key metadata for provider setup #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct ConfigKey { + /// The name of the configuration key (e.g., "API_KEY") pub name: String, + /// Whether this key is required for the provider to function pub required: bool, + /// Whether this key should be stored securely (e.g., in keychain) pub secret: bool, + /// Optional default value for the key pub default: Option, + /// Whether this key should be configured using OAuth device code flow + /// When true, the provider's configure_oauth() method will be called instead of prompting for manual input + pub oauth_flow: bool, } impl ConfigKey { + /// Create a new ConfigKey pub fn new(name: &str, required: bool, secret: bool, default: Option<&str>) -> Self { Self { name: name.to_string(), required, secret, default: default.map(|s| s.to_string()), + oauth_flow: false, + } + } + + /// Create a new ConfigKey that uses OAuth device code flow for configuration + /// + /// This is used for providers that support OAuth authentication instead of manual API key entry. + /// When oauth_flow is true, the configuration system will call the provider's configure_oauth() method. + pub fn new_oauth(name: &str, required: bool, secret: bool, default: Option<&str>) -> Self { + Self { + name: name.to_string(), + required, + secret, + default: default.map(|s| s.to_string()), + oauth_flow: true, } } } @@ -383,6 +407,23 @@ pub trait Provider: Send + Sync { } prompt } + + /// Configure OAuth authentication for this provider + /// + /// This method is called when a provider has configuration keys marked with oauth_flow = true. + /// Providers that support OAuth should override this method to implement their specific OAuth flow. + /// + /// # Returns + /// * `Ok(())` if OAuth configuration succeeds and credentials are saved + /// * `Err(ProviderError)` if OAuth fails or is not supported by this provider + /// + /// # Default Implementation + /// The default implementation returns an error indicating OAuth is not supported. + async fn configure_oauth(&self) -> Result<(), ProviderError> { + Err(ProviderError::ExecutionError( + "OAuth configuration not supported by this provider".to_string(), + )) + } } /// A message stream yields partial text content but complete tool calls, all within the Message object diff --git a/crates/goose/src/providers/githubcopilot.rs b/crates/goose/src/providers/githubcopilot.rs index 899ce0cb4033..10fda191752b 100644 --- a/crates/goose/src/providers/githubcopilot.rs +++ b/crates/goose/src/providers/githubcopilot.rs @@ -386,7 +386,12 @@ impl Provider for GithubCopilotProvider { GITHUB_COPILOT_DEFAULT_MODEL, GITHUB_COPILOT_KNOWN_MODELS.to_vec(), GITHUB_COPILOT_DOC_URL, - vec![ConfigKey::new("GITHUB_COPILOT_TOKEN", true, true, None)], + vec![ConfigKey::new_oauth( + "GITHUB_COPILOT_TOKEN", + true, + true, + None, + )], ) } @@ -461,4 +466,33 @@ impl Provider for GithubCopilotProvider { models.sort(); Ok(Some(models)) } + + async fn configure_oauth(&self) -> Result<(), ProviderError> { + let config = Config::global(); + + // Check if token already exists and is valid + if config.get_secret::("GITHUB_COPILOT_TOKEN").is_ok() { + // Try to refresh API info to validate the token + match self.refresh_api_info().await { + Ok(_) => return Ok(()), // Token is valid + Err(_) => { + // Token is invalid, continue with OAuth flow + tracing::debug!("Existing token is invalid, starting OAuth flow"); + } + } + } + + // Start OAuth device code flow + let token = self + .get_access_token() + .await + .map_err(|e| ProviderError::Authentication(format!("OAuth flow failed: {}", e)))?; + + // Save the token + config + .set_secret("GITHUB_COPILOT_TOKEN", Value::String(token)) + .map_err(|e| ProviderError::ExecutionError(format!("Failed to save token: {}", e)))?; + + Ok(()) + } } diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index f032e086b789..c284de8a7b50 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -1150,24 +1150,34 @@ }, "ConfigKey": { "type": "object", + "description": "Configuration key metadata for provider setup", "required": [ "name", "required", - "secret" + "secret", + "oauth_flow" ], "properties": { "default": { "type": "string", + "description": "Optional default value for the key", "nullable": true }, "name": { - "type": "string" + "type": "string", + "description": "The name of the configuration key (e.g., \"API_KEY\")" + }, + "oauth_flow": { + "type": "boolean", + "description": "Whether this key should be configured using OAuth device code flow\nWhen true, the provider's configure_oauth() method will be called instead of prompting for manual input" }, "required": { - "type": "boolean" + "type": "boolean", + "description": "Whether this key is required for the provider to function" }, "secret": { - "type": "boolean" + "type": "boolean", + "description": "Whether this key should be stored securely (e.g., in keychain)" } } }, diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 442ac0d247f0..540d082b9626 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -26,10 +26,30 @@ export type AuthorRequest = { metadata?: string | null; }; +/** + * Configuration key metadata for provider setup + */ export type ConfigKey = { + /** + * Optional default value for the key + */ default?: string | null; + /** + * The name of the configuration key (e.g., "API_KEY") + */ name: string; + /** + * Whether this key should be configured using OAuth device code flow + * When true, the provider's configure_oauth() method will be called instead of prompting for manual input + */ + oauth_flow: boolean; + /** + * Whether this key is required for the provider to function + */ required: boolean; + /** + * Whether this key should be stored securely (e.g., in keychain) + */ secret: boolean; };