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
9 changed files with 147 additions and 7 deletions
Generated
+84
View File
@@ -202,6 +202,26 @@ dependencies = [
"derive_arbitrary", "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]] [[package]]
name = "arc-swap" name = "arc-swap"
version = "1.9.0" version = "1.9.0"
@@ -4199,6 +4219,16 @@ dependencies = [
"zeroize", "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]] [[package]]
name = "getopts" name = "getopts"
version = "0.2.24" version = "0.2.24"
@@ -4301,6 +4331,7 @@ dependencies = [
"agent-client-protocol-schema", "agent-client-protocol-schema",
"ahash", "ahash",
"anyhow", "anyhow",
"arboard",
"async-stream", "async-stream",
"async-trait", "async-trait",
"aws-config", "aws-config",
@@ -6432,6 +6463,18 @@ dependencies = [
"objc2-encode", "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]] [[package]]
name = "objc2-core-foundation" name = "objc2-core-foundation"
version = "0.3.2" version = "0.3.2"
@@ -6443,6 +6486,19 @@ dependencies = [
"objc2", "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]] [[package]]
name = "objc2-encode" name = "objc2-encode"
version = "4.1.0" version = "4.1.0"
@@ -6462,6 +6518,17 @@ dependencies = [
"objc2-core-foundation", "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]] [[package]]
name = "objc2-metal" name = "objc2-metal"
version = "0.3.2" version = "0.3.2"
@@ -12367,6 +12434,23 @@ dependencies = [
"tap", "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]] [[package]]
name = "x509-cert" name = "x509-cert"
version = "0.2.5" version = "0.2.5"
+1
View File
@@ -22,6 +22,7 @@ string_slice = "warn"
rmcp = { version = "1.2.0", features = ["schemars", "auth"] } rmcp = { version = "1.2.0", features = ["schemars", "auth"] }
agent-client-protocol-schema = { version = "0.11", features = ["unstable"] } agent-client-protocol-schema = { version = "0.11", features = ["unstable"] }
sacp = "11.0.0" sacp = "11.0.0"
arboard = "3"
anyhow = "1.0" anyhow = "1.0"
async-stream = "0.3" async-stream = "0.3"
async-trait = "0.1" async-trait = "0.1"
+1
View File
@@ -69,6 +69,7 @@ rmcp = { workspace = true, features = [
"transport-streamable-http-client-reqwest", "transport-streamable-http-client-reqwest",
] } ] }
oauth2 = { version = "5.0", default-features = false } oauth2 = { version = "5.0", default-features = false }
arboard = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
futures = { workspace = true } futures = { workspace = true }
+31 -2
View File
@@ -266,9 +266,13 @@ pub struct ConfigKey {
pub secret: bool, pub secret: bool,
/// Optional default value for the key /// 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 /// 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 /// When true, the provider's configure_oauth() method will be called instead of prompting for manual input
pub oauth_flow: bool, 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 /// Whether this key should be shown prominently during provider setup
/// (onboarding, settings modal, CLI configure) /// (onboarding, settings modal, CLI configure)
#[serde(default)] #[serde(default)]
@@ -290,6 +294,7 @@ impl ConfigKey {
secret, secret,
default: default.map(|s| s.to_string()), default: default.map(|s| s.to_string()),
oauth_flow: false, oauth_flow: false,
device_code_flow: false,
primary, primary,
} }
} }
@@ -301,11 +306,12 @@ impl ConfigKey {
secret, secret,
default: Some(T::DEFAULT.to_string()), default: Some(T::DEFAULT.to_string()),
oauth_flow: false, oauth_flow: false,
device_code_flow: false,
primary, 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. /// 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. /// When oauth_flow is true, the configuration system will call the provider's configure_oauth() method.
@@ -322,6 +328,29 @@ impl ConfigKey {
secret, secret,
default: default.map(|s| s.to_string()), default: default.map(|s| s.to_string()),
oauth_flow: true, 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, primary,
} }
} }
+11 -1
View File
@@ -285,6 +285,16 @@ impl GithubCopilotProvider {
async fn login(&self) -> Result<String> { async fn login(&self) -> Result<String> {
let device_code_info = self.get_device_code().await?; 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!( println!(
"Please visit {} and enter code {}", "Please visit {} and enter code {}",
device_code_info.verification_uri, device_code_info.user_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_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_oauth( vec![ConfigKey::new_oauth_device_code(
"GITHUB_COPILOT_TOKEN", "GITHUB_COPILOT_TOKEN",
true, true,
true, true,
+5 -1
View File
@@ -4157,13 +4157,17 @@
"description": "Optional default value for the key", "description": "Optional default value for the key",
"nullable": true "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": { "name": {
"type": "string", "type": "string",
"description": "The name of the configuration key (e.g., \"API_KEY\")" "description": "The name of the configuration key (e.g., \"API_KEY\")"
}, },
"oauth_flow": { "oauth_flow": {
"type": "boolean", "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": { "primary": {
"type": "boolean", "type": "boolean",
+6 -1
View File
@@ -90,12 +90,17 @@ export type ConfigKey = {
* Optional default value for the key * Optional default value for the key
*/ */
default?: string | null; 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") * 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 * 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 * When true, the provider's configure_oauth() method will be called instead of prompting for manual input
*/ */
oauth_flow: boolean; oauth_flow: boolean;
@@ -56,6 +56,8 @@ function OAuthForm({
} }
}; };
const isDeviceCodeFlow = provider.metadata.config_keys.some((key) => key.device_code_flow);
return ( return (
<div className="flex flex-col items-center gap-3 py-4"> <div className="flex flex-col items-center gap-3 py-4">
<Button <Button
@@ -68,7 +70,9 @@ function OAuthForm({
{isLoading ? 'Signing in...' : `Sign in with ${provider.metadata.display_name}`} {isLoading ? 'Signing in...' : `Sign in with ${provider.metadata.display_name}`}
</Button> </Button>
<p className="text-xs text-text-muted text-center"> <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> </p>
</div> </div>
); );
@@ -281,7 +281,9 @@ export default function ProviderConfigurationModal({
: `Sign in with ${provider.metadata.display_name}`} : `Sign in with ${provider.metadata.display_name}`}
</Button> </Button>
<p className="text-sm text-text-secondary text-center"> <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> </p>
</div> </div>
) : provider.metadata.config_keys.length === 0 && ) : provider.metadata.config_keys.length === 0 &&