fix: GitHub Copilot auth fails to open browser in Desktop app (#6957) (#8019)

Signed-off-by: Douwe Osinga <douwe@squareup.com>
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
jh-block 2026-03-26 19:03:44 +01:00 committed by GitHub
parent 2977512bb5
commit 11f46dc0e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 147 additions and 7 deletions

84
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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 }

View file

@ -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)]
@ -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,
}
}

View file

@ -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);
}
}
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,

View file

@ -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",

View file

@ -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;

View file

@ -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
@ -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>
);

View file

@ -281,7 +281,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>
) : provider.metadata.config_keys.length === 0 &&