mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
feat: add OAuth provider abstraction for CLI configuration (#3157)
Signed-off-by: Adam Tarantino <tarantino.adam@gmail.com>
This commit is contained in:
+1
-1
@@ -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
|
||||
|
||||
@@ -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))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user