mirror of
https://github.com/openai/codex.git
synced 2026-04-30 03:12:20 +03:00
Add ChatGPT device-code login to app server (#15525)
## Problem
App-server clients could only initiate ChatGPT login through the browser
callback flow, even though the shared login crate already supports
device-code auth. That left VS Code, Codex App, and other app-server
clients without a first-class way to use the existing device-code
backend when browser redirects are brittle or when the client UX wants
to own the login ceremony.
## Mental model
This change adds a second ChatGPT login start path to app-server:
clients can now call `account/login/start` with `type:
"chatgptDeviceCode"`. App-server immediately returns a `loginId` plus
the device-code UX payload (`verificationUrl` and `userCode`), then
completes the login asynchronously in the background using the existing
`codex_login` polling flow. Successful device-code login still resolves
to ordinary `chatgpt` auth, and completion continues to flow through the
existing `account/login/completed` and `account/updated` notifications.
## Non-goals
This does not introduce a new auth mode, a new account shape, or a
device-code eligibility discovery API. It also does not add automatic
fallback to browser login in core; clients remain responsible for
choosing when to request device code and whether to retry with a
different UX if the backend/admin policy rejects it.
## Tradeoffs
We intentionally keep `login_chatgpt_common` as a local validation
helper instead of turning it into a capability probe. Device-code
eligibility is checked by actually calling `request_device_code`, which
means policy-disabled cases surface as an immediate request error rather
than an async completion event. We also keep the active-login state
machine minimal: browser and device-code logins share the same public
cancel contract, but device-code cancellation is implemented with a
local cancel token rather than a larger cross-crate refactor.
## Architecture
The protocol grows a new `chatgptDeviceCode` request/response variant in
app-server v2. On the server side, the new handler reuses the existing
ChatGPT login precondition checks, calls `request_device_code`, returns
the device-code payload, and then spawns a background task that waits on
either cancellation or `complete_device_code_login`. On success, it
reuses the existing auth reload and cloud-requirements refresh path
before emitting `account/login/completed` success and `account/updated`.
On failure or cancellation, it emits only `account/login/completed`
failure. The existing `account/login/cancel { loginId }` contract
remains unchanged and now works for both browser and device-code
attempts.
## Tests
Added protocol serialization coverage for the new request/response
variant, plus app-server tests for device-code success, failure, cancel,
and start-time rejection behavior. Existing browser ChatGPT login
coverage remains in place to show that the callback-based flow is
unchanged.
This commit is contained in:
@@ -225,7 +225,11 @@ enum CliCommand {
|
||||
abort_on: Option<usize>,
|
||||
},
|
||||
/// Trigger the ChatGPT login flow and wait for completion.
|
||||
TestLogin,
|
||||
TestLogin {
|
||||
/// Use the device-code login flow instead of the browser callback flow.
|
||||
#[arg(long, default_value_t = false)]
|
||||
device_code: bool,
|
||||
},
|
||||
/// Fetch the current account rate limits from the Codex app-server.
|
||||
GetAccountRateLimits,
|
||||
/// List the available models from the Codex app-server.
|
||||
@@ -372,10 +376,10 @@ pub async fn run() -> Result<()> {
|
||||
)
|
||||
.await
|
||||
}
|
||||
CliCommand::TestLogin => {
|
||||
CliCommand::TestLogin { device_code } => {
|
||||
ensure_dynamic_tools_unused(&dynamic_tools, "test-login")?;
|
||||
let endpoint = resolve_endpoint(codex_bin, url)?;
|
||||
test_login(&endpoint, &config_overrides).await
|
||||
test_login(&endpoint, &config_overrides, device_code).await
|
||||
}
|
||||
CliCommand::GetAccountRateLimits => {
|
||||
ensure_dynamic_tools_unused(&dynamic_tools, "get-account-rate-limits")?;
|
||||
@@ -1028,17 +1032,38 @@ async fn send_follow_up_v2(
|
||||
.await
|
||||
}
|
||||
|
||||
async fn test_login(endpoint: &Endpoint, config_overrides: &[String]) -> Result<()> {
|
||||
async fn test_login(
|
||||
endpoint: &Endpoint,
|
||||
config_overrides: &[String],
|
||||
device_code: bool,
|
||||
) -> Result<()> {
|
||||
with_client("test-login", endpoint, config_overrides, |client| {
|
||||
let initialize = client.initialize()?;
|
||||
println!("< initialize response: {initialize:?}");
|
||||
|
||||
let login_response = client.login_account_chatgpt()?;
|
||||
println!("< account/login/start response: {login_response:?}");
|
||||
let LoginAccountResponse::Chatgpt { login_id, auth_url } = login_response else {
|
||||
bail!("expected chatgpt login response");
|
||||
let login_response = if device_code {
|
||||
client.login_account_chatgpt_device_code()?
|
||||
} else {
|
||||
client.login_account_chatgpt()?
|
||||
};
|
||||
println!("< account/login/start response: {login_response:?}");
|
||||
let login_id = match login_response {
|
||||
LoginAccountResponse::Chatgpt { login_id, auth_url } => {
|
||||
println!("Open the following URL in your browser to continue:\n{auth_url}");
|
||||
login_id
|
||||
}
|
||||
LoginAccountResponse::ChatgptDeviceCode {
|
||||
login_id,
|
||||
verification_url,
|
||||
user_code,
|
||||
} => {
|
||||
println!(
|
||||
"Open the following URL and enter the code to continue:\n{verification_url}\n\nCode: {user_code}"
|
||||
);
|
||||
login_id
|
||||
}
|
||||
_ => bail!("expected chatgpt login response"),
|
||||
};
|
||||
println!("Open the following URL in your browser to continue:\n{auth_url}");
|
||||
|
||||
let completion = client.wait_for_account_login_completion(&login_id)?;
|
||||
println!("< account/login/completed notification: {completion:?}");
|
||||
@@ -1590,6 +1615,16 @@ impl CodexClient {
|
||||
self.send_request(request, request_id, "account/login/start")
|
||||
}
|
||||
|
||||
fn login_account_chatgpt_device_code(&mut self) -> Result<LoginAccountResponse> {
|
||||
let request_id = self.request_id();
|
||||
let request = ClientRequest::LoginAccount {
|
||||
request_id: request_id.clone(),
|
||||
params: codex_app_server_protocol::LoginAccountParams::ChatgptDeviceCode,
|
||||
};
|
||||
|
||||
self.send_request(request, request_id, "account/login/start")
|
||||
}
|
||||
|
||||
fn get_account_rate_limits(&mut self) -> Result<GetAccountRateLimitsResponse> {
|
||||
let request_id = self.request_id();
|
||||
let request = ClientRequest::GetAccountRateLimits {
|
||||
|
||||
Reference in New Issue
Block a user