Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
113 changes: 80 additions & 33 deletions crates/goose-cli/src/commands/configure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -313,49 +346,63 @@ 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());
}
input.interact()?
};

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))?;
}
}
}
Expand Down
41 changes: 41 additions & 0 deletions crates/goose/src/providers/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<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,
}
}
}
Expand Down Expand Up @@ -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
Expand Down
36 changes: 35 additions & 1 deletion crates/goose/src/providers/githubcopilot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)],
)
}

Expand Down Expand Up @@ -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::<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(())
}
}
18 changes: 14 additions & 4 deletions ui/desktop/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
}
}
},
Expand Down
20 changes: 20 additions & 0 deletions ui/desktop/src/api/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down
Loading