Skip to content
Open
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
84 changes: 84 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ string_slice = "warn"
[workspace.dependencies]
rmcp = { version = "1.2.0", features = ["schemars", "auth"] }
sacp = "10.1.0"
arboard = "3"
anyhow = "1.0"
async-stream = "0.3"
async-trait = "0.1"
Expand Down
1 change: 1 addition & 0 deletions crates/goose/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ rmcp = { workspace = true, features = [
"transport-streamable-http-client-reqwest",
] }
oauth2 = "5.0"
arboard = { workspace = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
futures = { workspace = true }
Expand Down
33 changes: 31 additions & 2 deletions crates/goose/src/providers/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,9 +266,13 @@ pub struct ConfigKey {
pub secret: bool,
/// Optional default value for the key
pub default: Option<String>,
/// Whether this key should be configured using OAuth device code flow
/// Whether this key should be configured using an OAuth flow
/// When true, the provider's configure_oauth() method will be called instead of prompting for manual input
pub oauth_flow: bool,
/// Whether this OAuth flow uses the device code grant (RFC 8628)
/// When true, the user must enter a verification code in the browser
#[serde(default)]
pub device_code_flow: bool,
/// Whether this key should be shown prominently during provider setup
/// (onboarding, settings modal, CLI configure)
#[serde(default)]
Expand All @@ -290,6 +294,7 @@ impl ConfigKey {
secret,
default: default.map(|s| s.to_string()),
oauth_flow: false,
device_code_flow: false,
primary,
}
}
Expand All @@ -301,11 +306,12 @@ impl ConfigKey {
secret,
default: Some(T::DEFAULT.to_string()),
oauth_flow: false,
device_code_flow: false,
primary,
}
}

/// Create a new ConfigKey that uses OAuth device code flow for configuration
/// Create a new ConfigKey that uses an OAuth 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.
Expand All @@ -322,6 +328,29 @@ impl ConfigKey {
secret,
default: default.map(|s| s.to_string()),
oauth_flow: true,
device_code_flow: false,
primary,
}
}

/// Create a new ConfigKey that uses OAuth device code flow (RFC 8628) for configuration
///
/// Similar to new_oauth, but indicates the provider uses the device code grant where the user
/// must enter a verification code in the browser.
pub fn new_oauth_device_code(
name: &str,
required: bool,
secret: bool,
default: Option<&str>,
primary: bool,
) -> Self {
Self {
name: name.to_string(),
required,
secret,
default: default.map(|s| s.to_string()),
oauth_flow: true,
device_code_flow: true,
primary,
}
}
Expand Down
12 changes: 11 additions & 1 deletion crates/goose/src/providers/githubcopilot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,16 @@ impl GithubCopilotProvider {
async fn login(&self) -> Result<String> {
let device_code_info = self.get_device_code().await?;

if let Ok(mut clipboard) = arboard::Clipboard::new() {
if let Err(e) = clipboard.set_text(&device_code_info.user_code) {
tracing::warn!("Failed to copy verification code to clipboard: {}", e);
}
}
Comment on lines +288 to +292

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Surface a manual fallback when clipboard automation fails

If clipboard access is unavailable here (common in WSL/headless Linux/some sandboxed desktop sessions), the only fallback is the later println!, but the Electron wrapper stops forwarding goosed stdout after startup (ui/desktop/src/goosed.ts:306-316) and the OAuth endpoint only returns after the whole flow completes (crates/goose-server/src/routes/config_management.rs:898-910). In those environments the user still sits on "Signing in..." with no verification code to enter manually.

Useful? React with 👍 / 👎.


if let Err(e) = webbrowser::open(&device_code_info.verification_uri) {
tracing::warn!("Failed to open browser: {}", e);
Comment on lines +294 to +295

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid launching Copilot auth on the backend machine

This opens the verification URL from inside goosed, but the desktop app explicitly supports remote/external backends (ui/desktop/src/goosed.ts:194-208). In that setup the browser opens on the server host instead of the user's workstation, and /config/providers/{name}/oauth does not stream the device code back to the renderer (crates/goose-server/src/routes/config_management.rs:898-910), so GitHub Copilot sign-in is still unusable for external-backend users.

Useful? React with 👍 / 👎.

}

println!(
"Please visit {} and enter code {}",
device_code_info.verification_uri, device_code_info.user_code
Expand Down Expand Up @@ -402,7 +412,7 @@ impl ProviderDef for GithubCopilotProvider {
GITHUB_COPILOT_DEFAULT_MODEL,
GITHUB_COPILOT_KNOWN_MODELS.to_vec(),
GITHUB_COPILOT_DOC_URL,
vec![ConfigKey::new_oauth(
vec![ConfigKey::new_oauth_device_code(
"GITHUB_COPILOT_TOKEN",
true,
true,
Expand Down
6 changes: 5 additions & 1 deletion ui/desktop/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -4170,13 +4170,17 @@
"description": "Optional default value for the key",
"nullable": true
},
"device_code_flow": {
"type": "boolean",
"description": "Whether this OAuth flow uses the device code grant (RFC 8628)\nWhen true, the user must enter a verification code in the browser"
},
"name": {
"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"
"description": "Whether this key should be configured using an OAuth flow\nWhen true, the provider's configure_oauth() method will be called instead of prompting for manual input"
},
"primary": {
"type": "boolean",
Expand Down
7 changes: 6 additions & 1 deletion ui/desktop/src/api/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,17 @@ export type ConfigKey = {
* Optional default value for the key
*/
default?: string | null;
/**
* Whether this OAuth flow uses the device code grant (RFC 8628)
* When true, the user must enter a verification code in the browser
*/
device_code_flow?: boolean;
/**
* The name of the configuration key (e.g., "API_KEY")
*/
name: string;
/**
* Whether this key should be configured using OAuth device code flow
* Whether this key should be configured using an OAuth flow
* When true, the provider's configure_oauth() method will be called instead of prompting for manual input
*/
oauth_flow: boolean;
Expand Down
6 changes: 5 additions & 1 deletion ui/desktop/src/components/onboarding/ProviderConfigForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ function OAuthForm({
}
};

const isDeviceCodeFlow = provider.metadata.config_keys.some((key) => key.device_code_flow);

return (
<div className="flex flex-col items-center gap-3 py-4">
<Button
Expand All @@ -68,7 +70,9 @@ function OAuthForm({
{isLoading ? 'Signing in...' : `Sign in with ${provider.metadata.display_name}`}
</Button>
<p className="text-xs text-text-muted text-center">
A browser window will open for you to complete the login.
{isDeviceCodeFlow
? 'A browser window will open and the verification code will be copied to your clipboard. Paste it in the browser to complete sign-in.'
: 'A browser window will open for you to complete the login.'}
</p>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,9 @@ export default function ProviderConfigurationModal({
: `Sign in with ${provider.metadata.display_name}`}
</Button>
<p className="text-sm text-text-secondary text-center">
A browser window will open for you to complete the login.
{provider.metadata.config_keys.some((key) => key.device_code_flow)
? 'A browser window will open and the verification code will be copied to your clipboard. Paste it in the browser to complete sign-in.'
: 'A browser window will open for you to complete the login.'}
</p>
</div>
) : (
Expand Down
Loading