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 check # do your changes compile
|
||||||
cargo test # do the tests pass with your changes
|
cargo test # do the tests pass with your changes
|
||||||
cargo fmt # format your code
|
cargo fmt # format your code
|
||||||
./scripts/clippy-lint # run the linter
|
./scripts/clippy-lint.sh # run the linter
|
||||||
```
|
```
|
||||||
|
|
||||||
### Node
|
### 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
|
/// Dialog for configuring the AI provider and model
|
||||||
pub async fn configure_provider_dialog() -> Result<bool, Box<dyn Error>> {
|
pub async fn configure_provider_dialog() -> Result<bool, Box<dyn Error>> {
|
||||||
// Get global config instance
|
// Get global config instance
|
||||||
@@ -313,13 +346,52 @@ pub async fn configure_provider_dialog() -> Result<bool, Box<dyn Error>> {
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let _ = cliclack::log::info(format!("{} is already configured", key.name));
|
let _ = cliclack::log::info(format!("{} is already configured", key.name));
|
||||||
if cliclack::confirm("Would you like to update this value?").interact()? {
|
if cliclack::confirm("Would you like to update this value?").interact()? {
|
||||||
let new_value: String = if key.secret {
|
// Check if this key uses OAuth flow
|
||||||
cliclack::password(format!("Enter new value for {}", key.name))
|
if key.oauth_flow {
|
||||||
.mask('▪')
|
handle_oauth_configuration(provider_name, &key.name).await?;
|
||||||
.interact()?
|
|
||||||
} else {
|
} else {
|
||||||
let mut input =
|
// Non-OAuth key, use manual entry
|
||||||
cliclack::input(format!("Enter new value for {}", key.name));
|
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() {
|
if key.default.is_some() {
|
||||||
input = input.default_input(&key.default.clone().unwrap());
|
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 {
|
if key.secret {
|
||||||
config.set_secret(&key.name, Value::String(new_value))?;
|
config.set_secret(&key.name, Value::String(value))?;
|
||||||
} else {
|
} 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)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct ConfigKey {
|
pub struct ConfigKey {
|
||||||
|
/// The name of the configuration key (e.g., "API_KEY")
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
/// Whether this key is required for the provider to function
|
||||||
pub required: bool,
|
pub required: bool,
|
||||||
|
/// Whether this key should be stored securely (e.g., in keychain)
|
||||||
pub secret: bool,
|
pub secret: bool,
|
||||||
|
/// Optional default value for the key
|
||||||
pub default: Option<String>,
|
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 {
|
impl ConfigKey {
|
||||||
|
/// Create a new ConfigKey
|
||||||
pub fn new(name: &str, required: bool, secret: bool, default: Option<&str>) -> Self {
|
pub fn new(name: &str, required: bool, secret: bool, default: Option<&str>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
required,
|
required,
|
||||||
secret,
|
secret,
|
||||||
default: default.map(|s| s.to_string()),
|
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
|
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
|
/// 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_DEFAULT_MODEL,
|
||||||
GITHUB_COPILOT_KNOWN_MODELS.to_vec(),
|
GITHUB_COPILOT_KNOWN_MODELS.to_vec(),
|
||||||
GITHUB_COPILOT_DOC_URL,
|
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();
|
models.sort();
|
||||||
Ok(Some(models))
|
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": {
|
"ConfigKey": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"description": "Configuration key metadata for provider setup",
|
||||||
"required": [
|
"required": [
|
||||||
"name",
|
"name",
|
||||||
"required",
|
"required",
|
||||||
"secret"
|
"secret",
|
||||||
|
"oauth_flow"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"default": {
|
"default": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"description": "Optional default value for the key",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
},
|
},
|
||||||
"name": {
|
"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": {
|
"required": {
|
||||||
"type": "boolean"
|
"type": "boolean",
|
||||||
|
"description": "Whether this key is required for the provider to function"
|
||||||
},
|
},
|
||||||
"secret": {
|
"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;
|
metadata?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration key metadata for provider setup
|
||||||
|
*/
|
||||||
export type ConfigKey = {
|
export type ConfigKey = {
|
||||||
|
/**
|
||||||
|
* Optional default value for the key
|
||||||
|
*/
|
||||||
default?: string | null;
|
default?: string | null;
|
||||||
|
/**
|
||||||
|
* The name of the configuration key (e.g., "API_KEY")
|
||||||
|
*/
|
||||||
name: string;
|
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;
|
required: boolean;
|
||||||
|
/**
|
||||||
|
* Whether this key should be stored securely (e.g., in keychain)
|
||||||
|
*/
|
||||||
secret: boolean;
|
secret: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user