feat: add OAuth provider abstraction for CLI configuration (#3157)

Signed-off-by: Adam Tarantino <tarantino.adam@gmail.com>
This commit is contained in:
Adam Tarantino
2025-08-06 08:04:49 -07:00
committed by GitHub
parent 90ef14979d
commit c2f08557af
6 changed files with 191 additions and 39 deletions
+1 -1
View File
@@ -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
+80 -33
View File
@@ -246,6 +246,39 @@ pub async fn handle_configure() -> Result<(), Box<dyn Error>> {
}
}
/// Helper function to handle OAuth configuration for a provider
async fn handle_oauth_configuration(
provider_name: &str,
key_name: &str,
) -> Result<(), Box<dyn std::error::Error>> {
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<bool, Box<dyn Error>> {
// Get global config instance
@@ -313,13 +346,52 @@ pub async fn configure_provider_dialog() -> Result<bool, Box<dyn Error>> {
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,37 +399,12 @@ pub async fn configure_provider_dialog() -> Result<bool, Box<dyn Error>> {
};
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))?;
config.set_param(&key.name, Value::String(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());
}
input.interact()?
};
if key.secret {
config.set_secret(&key.name, Value::String(value))?;
} else {
config.set_param(&key.name, Value::String(value))?;
}
}
}
}
}
+41
View File
@@ -163,21 +163,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<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
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,
}
}
}
@@ -388,6 +412,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
+35 -1
View File
@@ -387,7 +387,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,
)],
)
}
@@ -466,4 +471,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::<String>("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(())
}
}
+14 -4
View File
@@ -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)"
}
}
},
+20
View File
@@ -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;
};