[App-server] Add account/login/cancel v2 endpoint (#6288)

Add `account/login/cancel` v2 endpoint for auth. this is similar
implementation to `cancelLoginChatgpt` v1 endpoint.
This commit is contained in:
Celia Chen
2025-11-05 17:13:55 -08:00
committed by GitHub
parent 4a1a7f9685
commit 229d18f4d2
6 changed files with 123 additions and 20 deletions

View File

@@ -148,6 +148,13 @@ client_request_definitions! {
response: v2::LoginAccountResponse,
},
#[serde(rename = "account/login/cancel")]
#[ts(rename = "account/login/cancel")]
CancelLoginAccount {
params: v2::CancelLoginAccountParams,
response: v2::CancelLoginAccountResponse,
},
#[serde(rename = "account/logout")]
#[ts(rename = "account/logout")]
LogoutAccount {
@@ -235,6 +242,7 @@ client_request_definitions! {
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
response: v1::LoginChatGptResponse,
},
// DEPRECATED in favor of CancelLoginAccount
CancelLoginChatGpt {
params: v1::CancelLoginChatGptParams,
response: v1::CancelLoginChatGptResponse,

View File

@@ -374,11 +374,9 @@ pub enum InputItem {
LocalImage { path: PathBuf },
}
// Deprecated in favor of AccountLoginCompletedNotification.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
/// Deprecated: use `account/login/completed` instead.
/// Deprecated in favor of AccountLoginCompletedNotification.
pub struct LoginChatGptCompleteNotification {
#[schemars(with = "String")]
pub login_id: Uuid,

View File

@@ -167,6 +167,18 @@ pub enum LoginAccountResponse {
},
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct CancelLoginAccountParams {
pub login_id: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct CancelLoginAccountResponse {}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]

View File

@@ -15,6 +15,9 @@ use codex_app_server_protocol::ArchiveConversationParams;
use codex_app_server_protocol::ArchiveConversationResponse;
use codex_app_server_protocol::AuthMode;
use codex_app_server_protocol::AuthStatusChangeNotification;
use codex_app_server_protocol::CancelLoginAccountParams;
use codex_app_server_protocol::CancelLoginAccountResponse;
use codex_app_server_protocol::CancelLoginChatGptResponse;
use codex_app_server_protocol::ClientRequest;
use codex_app_server_protocol::ConversationSummary;
use codex_app_server_protocol::ExecCommandApprovalParams;
@@ -233,6 +236,9 @@ impl CodexMessageProcessor {
} => {
self.logout_v2(request_id).await;
}
ClientRequest::CancelLoginAccount { request_id, params } => {
self.cancel_login_v2(request_id, params).await;
}
ClientRequest::GetAccount {
request_id,
params: _,
@@ -644,27 +650,59 @@ impl CodexMessageProcessor {
}
}
async fn cancel_login_chatgpt(&mut self, request_id: RequestId, login_id: Uuid) {
async fn cancel_login_chatgpt_common(
&mut self,
login_id: Uuid,
) -> std::result::Result<(), JSONRPCErrorError> {
let mut guard = self.active_login.lock().await;
if guard.as_ref().map(|l| l.login_id) == Some(login_id) {
if let Some(active) = guard.take() {
active.drop();
}
drop(guard);
self.outgoing
.send_response(
request_id,
codex_app_server_protocol::CancelLoginChatGptResponse {},
)
.await;
Ok(())
} else {
drop(guard);
let error = JSONRPCErrorError {
Err(JSONRPCErrorError {
code: INVALID_REQUEST_ERROR_CODE,
message: format!("login id not found: {login_id}"),
data: None,
};
self.outgoing.send_error(request_id, error).await;
})
}
}
async fn cancel_login_chatgpt(&mut self, request_id: RequestId, login_id: Uuid) {
match self.cancel_login_chatgpt_common(login_id).await {
Ok(()) => {
self.outgoing
.send_response(request_id, CancelLoginChatGptResponse {})
.await;
}
Err(error) => {
self.outgoing.send_error(request_id, error).await;
}
}
}
async fn cancel_login_v2(&mut self, request_id: RequestId, params: CancelLoginAccountParams) {
let login_id = params.login_id;
match Uuid::parse_str(&login_id) {
Ok(uuid) => match self.cancel_login_chatgpt_common(uuid).await {
Ok(()) => {
self.outgoing
.send_response(request_id, CancelLoginAccountResponse {})
.await;
}
Err(error) => {
self.outgoing.send_error(request_id, error).await;
}
},
Err(_) => {
let error = JSONRPCErrorError {
code: INVALID_REQUEST_ERROR_CODE,
message: format!("invalid login id: {login_id}"),
data: None,
};
self.outgoing.send_error(request_id, error).await;
}
}
}

View File

@@ -14,6 +14,7 @@ use anyhow::Context;
use assert_cmd::prelude::*;
use codex_app_server_protocol::AddConversationListenerParams;
use codex_app_server_protocol::ArchiveConversationParams;
use codex_app_server_protocol::CancelLoginAccountParams;
use codex_app_server_protocol::CancelLoginChatGptParams;
use codex_app_server_protocol::ClientInfo;
use codex_app_server_protocol::ClientNotification;
@@ -386,6 +387,15 @@ impl McpProcess {
self.send_request("account/login/start", Some(params)).await
}
/// Send an `account/login/cancel` JSON-RPC request.
pub async fn send_cancel_login_account_request(
&mut self,
params: CancelLoginAccountParams,
) -> anyhow::Result<i64> {
let params = Some(serde_json::to_value(params)?);
self.send_request("account/login/cancel", params).await
}
/// Send a `fuzzyFileSearch` JSON-RPC request.
pub async fn send_fuzzy_file_search_request(
&mut self,

View File

@@ -3,6 +3,8 @@ use anyhow::bail;
use app_test_support::McpProcess;
use app_test_support::to_response;
use codex_app_server_protocol::AuthMode;
use codex_app_server_protocol::CancelLoginAccountParams;
use codex_app_server_protocol::CancelLoginAccountResponse;
use codex_app_server_protocol::GetAuthStatusParams;
use codex_app_server_protocol::GetAuthStatusResponse;
use codex_app_server_protocol::JSONRPCError;
@@ -16,6 +18,7 @@ use codex_login::login_with_api_key;
use pretty_assertions::assert_eq;
use serial_test::serial;
use std::path::Path;
use std::time::Duration;
use tempfile::TempDir;
use tokio::time::timeout;
@@ -229,17 +232,51 @@ async fn login_account_chatgpt_start() -> Result<()> {
.await??;
let login: LoginAccountResponse = to_response(resp)?;
let LoginAccountResponse::Chatgpt {
login_id: _,
auth_url,
} = login
else {
let LoginAccountResponse::Chatgpt { login_id, auth_url } = login else {
bail!("unexpected login response: {login:?}");
};
assert!(
auth_url.contains("redirect_uri=http%3A%2F%2Flocalhost"),
"auth_url should contain a redirect_uri to localhost"
);
let cancel_id = mcp
.send_cancel_login_account_request(CancelLoginAccountParams {
login_id: login_id.clone(),
})
.await?;
let cancel_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(cancel_id)),
)
.await??;
let _ok: CancelLoginAccountResponse = to_response(cancel_resp)?;
let note = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("account/login/completed"),
)
.await??;
let parsed: ServerNotification = note.try_into()?;
let ServerNotification::AccountLoginCompleted(payload) = parsed else {
bail!("unexpected notification: {parsed:?}");
};
pretty_assertions::assert_eq!(payload.login_id, Some(login_id));
pretty_assertions::assert_eq!(payload.success, false);
assert!(
payload.error.is_some(),
"expected a non-empty error on cancel"
);
let maybe_updated = timeout(
Duration::from_millis(500),
mcp.read_stream_until_notification_message("account/updated"),
)
.await;
assert!(
maybe_updated.is_err(),
"account/updated should not be emitted when login is cancelled"
);
Ok(())
}