From 11f46dc0e07e4618cf6e9f80665d72f6ae9e1868 Mon Sep 17 00:00:00 2001 From: jh-block Date: Thu, 26 Mar 2026 19:03:44 +0100 Subject: [PATCH] fix: GitHub Copilot auth fails to open browser in Desktop app (#6957) (#8019) Signed-off-by: Douwe Osinga Co-authored-by: Douwe Osinga --- Cargo.lock | 84 +++++++++++++++++++ Cargo.toml | 1 + crates/goose/Cargo.toml | 1 + crates/goose/src/providers/base.rs | 33 +++++++- crates/goose/src/providers/githubcopilot.rs | 12 ++- ui/desktop/openapi.json | 6 +- ui/desktop/src/api/types.gen.ts | 7 +- .../onboarding/ProviderConfigForm.tsx | 6 +- .../modal/ProviderConfiguationModal.tsx | 4 +- 9 files changed, 147 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b6da45e916..1fc6b5ad83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -202,6 +202,26 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image 0.25.10", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "x11rb", +] + [[package]] name = "arc-swap" version = "1.9.0" @@ -4199,6 +4219,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.4", + "windows-link", +] + [[package]] name = "getopts" version = "0.2.24" @@ -4301,6 +4331,7 @@ dependencies = [ "agent-client-protocol-schema", "ahash", "anyhow", + "arboard", "async-stream", "async-trait", "aws-config", @@ -6432,6 +6463,18 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-graphics", + "objc2-foundation", +] + [[package]] name = "objc2-core-foundation" version = "0.3.2" @@ -6443,6 +6486,19 @@ dependencies = [ "objc2", ] +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + [[package]] name = "objc2-encode" version = "4.1.0" @@ -6462,6 +6518,17 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", +] + [[package]] name = "objc2-metal" version = "0.3.2" @@ -12367,6 +12434,23 @@ dependencies = [ "tap", ] +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix 1.1.4", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + [[package]] name = "x509-cert" version = "0.2.5" diff --git a/Cargo.toml b/Cargo.toml index c8371a06b7..697cd51495 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ string_slice = "warn" rmcp = { version = "1.2.0", features = ["schemars", "auth"] } agent-client-protocol-schema = { version = "0.11", features = ["unstable"] } sacp = "11.0.0" +arboard = "3" anyhow = "1.0" async-stream = "0.3" async-trait = "0.1" diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml index 30541339bb..c1194c178a 100644 --- a/crates/goose/Cargo.toml +++ b/crates/goose/Cargo.toml @@ -69,6 +69,7 @@ rmcp = { workspace = true, features = [ "transport-streamable-http-client-reqwest", ] } oauth2 = { version = "5.0", default-features = false } +arboard = { workspace = true } anyhow = { workspace = true } thiserror = { workspace = true } futures = { workspace = true } diff --git a/crates/goose/src/providers/base.rs b/crates/goose/src/providers/base.rs index eb3516ce61..ccaf7263ab 100644 --- a/crates/goose/src/providers/base.rs +++ b/crates/goose/src/providers/base.rs @@ -266,9 +266,13 @@ pub struct ConfigKey { pub secret: bool, /// Optional default value for the key pub default: Option, - /// 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)] @@ -290,6 +294,7 @@ impl ConfigKey { secret, default: default.map(|s| s.to_string()), oauth_flow: false, + device_code_flow: false, primary, } } @@ -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. @@ -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, } } diff --git a/crates/goose/src/providers/githubcopilot.rs b/crates/goose/src/providers/githubcopilot.rs index 3c7c396578..fed31af79b 100644 --- a/crates/goose/src/providers/githubcopilot.rs +++ b/crates/goose/src/providers/githubcopilot.rs @@ -285,6 +285,16 @@ impl GithubCopilotProvider { async fn login(&self) -> Result { 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); + } + } + + if let Err(e) = webbrowser::open(&device_code_info.verification_uri) { + tracing::warn!("Failed to open browser: {}", e); + } + println!( "Please visit {} and enter code {}", device_code_info.verification_uri, device_code_info.user_code @@ -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, diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index f16e659781..a01501492b 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -4157,13 +4157,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", diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index eaffc44e83..5bc1990127 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -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; diff --git a/ui/desktop/src/components/onboarding/ProviderConfigForm.tsx b/ui/desktop/src/components/onboarding/ProviderConfigForm.tsx index b01b382d35..16c901838a 100644 --- a/ui/desktop/src/components/onboarding/ProviderConfigForm.tsx +++ b/ui/desktop/src/components/onboarding/ProviderConfigForm.tsx @@ -56,6 +56,8 @@ function OAuthForm({ } }; + const isDeviceCodeFlow = provider.metadata.config_keys.some((key) => key.device_code_flow); + return (
); diff --git a/ui/desktop/src/components/settings/providers/modal/ProviderConfiguationModal.tsx b/ui/desktop/src/components/settings/providers/modal/ProviderConfiguationModal.tsx index f46bef1b8f..5006ec7ce0 100644 --- a/ui/desktop/src/components/settings/providers/modal/ProviderConfiguationModal.tsx +++ b/ui/desktop/src/components/settings/providers/modal/ProviderConfiguationModal.tsx @@ -281,7 +281,9 @@ export default function ProviderConfigurationModal({ : `Sign in with ${provider.metadata.display_name}`}

- 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.'}

) : provider.metadata.config_keys.length === 0 &&