Skip to content

Commit 55c935a

Browse files
authored
[App-server] v2 for account/updated and account/logout (openai#6175)
V2 for `account/updated` and `account/logout` for app server. correspond to old `authStatusChange` and `LogoutChatGpt` respectively. Followup PRs will make other v2 endpoints call `account/updated` instead of `authStatusChange` too.
1 parent e256bdf commit 55c935a

File tree

11 files changed

+200
-26
lines changed

11 files changed

+200
-26
lines changed

codex-rs/app-server-protocol/src/export.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ macro_rules! for_each_schema_type {
4444
$macro!(crate::ArchiveConversationParams);
4545
$macro!(crate::ArchiveConversationResponse);
4646
$macro!(crate::AuthMode);
47+
$macro!(crate::AccountUpdatedNotification);
4748
$macro!(crate::AuthStatusChangeNotification);
4849
$macro!(crate::CancelLoginChatGptParams);
4950
$macro!(crate::CancelLoginChatGptResponse);

codex-rs/app-server-protocol/src/protocol/common.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,11 @@ pub struct FuzzyFileSearchResponse {
372372
#[strum(serialize_all = "camelCase")]
373373
pub enum ServerNotification {
374374
/// NEW NOTIFICATIONS
375+
#[serde(rename = "account/updated")]
376+
#[ts(rename = "account/updated")]
377+
#[strum(serialize = "account/updated")]
378+
AccountUpdated(v2::AccountUpdatedNotification),
379+
375380
#[serde(rename = "account/rateLimits/updated")]
376381
#[ts(rename = "account/rateLimits/updated")]
377382
#[strum(serialize = "account/rateLimits/updated")]
@@ -391,6 +396,7 @@ pub enum ServerNotification {
391396
impl ServerNotification {
392397
pub fn to_params(self) -> Result<serde_json::Value, serde_json::Error> {
393398
match self {
399+
ServerNotification::AccountUpdated(params) => serde_json::to_value(params),
394400
ServerNotification::AccountRateLimitsUpdated(params) => serde_json::to_value(params),
395401
ServerNotification::AuthStatusChange(params) => serde_json::to_value(params),
396402
ServerNotification::LoginChatGptComplete(params) => serde_json::to_value(params),

codex-rs/app-server-protocol/src/protocol/v1.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ pub struct SessionConfiguredNotification {
400400

401401
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
402402
#[serde(rename_all = "camelCase")]
403+
/// Deprecated notification. Use AccountUpdatedNotification instead.
403404
pub struct AuthStatusChangeNotification {
404405
pub auth_method: Option<AuthMode>,
405406
}

codex-rs/app-server-protocol/src/protocol/v2.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::protocol::common::AuthMode;
12
use codex_protocol::ConversationId;
23
use codex_protocol::account::PlanType;
34
use codex_protocol::config_types::ReasoningEffort;
@@ -120,3 +121,9 @@ pub struct UploadFeedbackParams {
120121
pub struct UploadFeedbackResponse {
121122
pub thread_id: String,
122123
}
124+
125+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
126+
#[serde(rename_all = "camelCase")]
127+
pub struct AccountUpdatedNotification {
128+
pub auth_method: Option<AuthMode>,
129+
}

codex-rs/app-server/src/codex_message_processor.rs

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::fuzzy_file_search::run_fuzzy_file_search;
44
use crate::models::supported_models;
55
use crate::outgoing_message::OutgoingMessageSender;
66
use crate::outgoing_message::OutgoingNotification;
7+
use codex_app_server_protocol::AccountUpdatedNotification;
78
use codex_app_server_protocol::AddConversationListenerParams;
89
use codex_app_server_protocol::AddConversationSubscriptionResponse;
910
use codex_app_server_protocol::ApplyPatchApprovalParams;
@@ -200,8 +201,7 @@ impl CodexMessageProcessor {
200201
request_id,
201202
params: _,
202203
} => {
203-
self.send_unimplemented_error(request_id, "account/logout")
204-
.await;
204+
self.logout_v2(request_id).await;
205205
}
206206
ClientRequest::GetAccount {
207207
request_id,
@@ -250,7 +250,7 @@ impl CodexMessageProcessor {
250250
request_id,
251251
params: _,
252252
} => {
253-
self.logout_chatgpt(request_id).await;
253+
self.logout_v1(request_id).await;
254254
}
255255
ClientRequest::GetAuthStatus { request_id, params } => {
256256
self.get_auth_status(request_id, params).await;
@@ -494,41 +494,71 @@ impl CodexMessageProcessor {
494494
}
495495
}
496496

497-
async fn logout_chatgpt(&mut self, request_id: RequestId) {
497+
async fn logout_common(&mut self) -> std::result::Result<Option<AuthMode>, JSONRPCErrorError> {
498+
// Cancel any active login attempt.
498499
{
499-
// Cancel any active login attempt.
500500
let mut guard = self.active_login.lock().await;
501501
if let Some(active) = guard.take() {
502502
active.drop();
503503
}
504504
}
505505

506506
if let Err(err) = self.auth_manager.logout() {
507-
let error = JSONRPCErrorError {
507+
return Err(JSONRPCErrorError {
508508
code: INTERNAL_ERROR_CODE,
509509
message: format!("logout failed: {err}"),
510510
data: None,
511-
};
512-
self.outgoing.send_error(request_id, error).await;
513-
return;
511+
});
514512
}
515513

516-
self.outgoing
517-
.send_response(
518-
request_id,
519-
codex_app_server_protocol::LogoutChatGptResponse {},
520-
)
521-
.await;
514+
// Reflect the current auth method after logout (likely None).
515+
Ok(self.auth_manager.auth().map(|auth| auth.mode))
516+
}
522517

523-
// Send auth status change notification reflecting the current auth mode
524-
// after logout.
525-
let current_auth_method = self.auth_manager.auth().map(|auth| auth.mode);
526-
let payload = AuthStatusChangeNotification {
527-
auth_method: current_auth_method,
528-
};
529-
self.outgoing
530-
.send_server_notification(ServerNotification::AuthStatusChange(payload))
531-
.await;
518+
async fn logout_v1(&mut self, request_id: RequestId) {
519+
match self.logout_common().await {
520+
Ok(current_auth_method) => {
521+
self.outgoing
522+
.send_response(
523+
request_id,
524+
codex_app_server_protocol::LogoutChatGptResponse {},
525+
)
526+
.await;
527+
528+
let payload = AuthStatusChangeNotification {
529+
auth_method: current_auth_method,
530+
};
531+
self.outgoing
532+
.send_server_notification(ServerNotification::AuthStatusChange(payload))
533+
.await;
534+
}
535+
Err(error) => {
536+
self.outgoing.send_error(request_id, error).await;
537+
}
538+
}
539+
}
540+
541+
async fn logout_v2(&mut self, request_id: RequestId) {
542+
match self.logout_common().await {
543+
Ok(current_auth_method) => {
544+
self.outgoing
545+
.send_response(
546+
request_id,
547+
codex_app_server_protocol::LogoutAccountResponse {},
548+
)
549+
.await;
550+
551+
let payload_v2 = AccountUpdatedNotification {
552+
auth_method: current_auth_method,
553+
};
554+
self.outgoing
555+
.send_server_notification(ServerNotification::AccountUpdated(payload_v2))
556+
.await;
557+
}
558+
Err(error) => {
559+
self.outgoing.send_error(request_id, error).await;
560+
}
561+
}
532562
}
533563

534564
async fn get_auth_status(

codex-rs/app-server/src/outgoing_message.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ pub(crate) struct OutgoingError {
141141

142142
#[cfg(test)]
143143
mod tests {
144+
use codex_app_server_protocol::AccountUpdatedNotification;
145+
use codex_app_server_protocol::AuthMode;
144146
use codex_app_server_protocol::LoginChatGptCompleteNotification;
145147
use codex_protocol::protocol::RateLimitSnapshot;
146148
use codex_protocol::protocol::RateLimitWindow;
@@ -204,4 +206,24 @@ mod tests {
204206
"ensure the notification serializes correctly"
205207
);
206208
}
209+
210+
#[test]
211+
fn verify_account_updated_notification_serialization() {
212+
let notification = ServerNotification::AccountUpdated(AccountUpdatedNotification {
213+
auth_method: Some(AuthMode::ApiKey),
214+
});
215+
216+
let jsonrpc_notification = OutgoingMessage::AppServerNotification(notification);
217+
assert_eq!(
218+
json!({
219+
"method": "account/updated",
220+
"params": {
221+
"authMethod": "apikey"
222+
},
223+
}),
224+
serde_json::to_value(jsonrpc_notification)
225+
.expect("ensure the notification serializes correctly"),
226+
"ensure the notification serializes correctly"
227+
);
228+
}
207229
}

codex-rs/app-server/tests/common/mcp_process.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,11 @@ impl McpProcess {
321321
self.send_request("logoutChatGpt", None).await
322322
}
323323

324+
/// Send an `account/logout` JSON-RPC request.
325+
pub async fn send_logout_account_request(&mut self) -> anyhow::Result<i64> {
326+
self.send_request("account/logout", None).await
327+
}
328+
324329
/// Send a `fuzzyFileSearch` JSON-RPC request.
325330
pub async fn send_fuzzy_file_search_request(
326331
&mut self,

codex-rs/app-server/tests/suite/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ mod send_message;
1313
mod set_default_model;
1414
mod user_agent;
1515
mod user_info;
16+
mod v2;
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
use anyhow::Result;
2+
use anyhow::bail;
3+
use app_test_support::McpProcess;
4+
use app_test_support::to_response;
5+
use codex_app_server_protocol::GetAuthStatusParams;
6+
use codex_app_server_protocol::GetAuthStatusResponse;
7+
use codex_app_server_protocol::JSONRPCResponse;
8+
use codex_app_server_protocol::LogoutAccountResponse;
9+
use codex_app_server_protocol::RequestId;
10+
use codex_app_server_protocol::ServerNotification;
11+
use codex_core::auth::AuthCredentialsStoreMode;
12+
use codex_login::login_with_api_key;
13+
use pretty_assertions::assert_eq;
14+
use std::path::Path;
15+
use tempfile::TempDir;
16+
use tokio::time::timeout;
17+
18+
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
19+
20+
// Helper to create a minimal config.toml for the app server
21+
fn create_config_toml(codex_home: &Path) -> std::io::Result<()> {
22+
let config_toml = codex_home.join("config.toml");
23+
std::fs::write(
24+
config_toml,
25+
r#"
26+
model = "mock-model"
27+
approval_policy = "never"
28+
sandbox_mode = "danger-full-access"
29+
30+
model_provider = "mock_provider"
31+
32+
[model_providers.mock_provider]
33+
name = "Mock provider for test"
34+
base_url = "http://127.0.0.1:0/v1"
35+
wire_api = "chat"
36+
request_max_retries = 0
37+
stream_max_retries = 0
38+
"#,
39+
)
40+
}
41+
42+
#[tokio::test]
43+
async fn logout_account_removes_auth_and_notifies() -> Result<()> {
44+
let codex_home = TempDir::new()?;
45+
create_config_toml(codex_home.path())?;
46+
47+
login_with_api_key(
48+
codex_home.path(),
49+
"sk-test-key",
50+
AuthCredentialsStoreMode::File,
51+
)?;
52+
assert!(codex_home.path().join("auth.json").exists());
53+
54+
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
55+
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
56+
57+
let id = mcp.send_logout_account_request().await?;
58+
let resp: JSONRPCResponse = timeout(
59+
DEFAULT_READ_TIMEOUT,
60+
mcp.read_stream_until_response_message(RequestId::Integer(id)),
61+
)
62+
.await??;
63+
let _ok: LogoutAccountResponse = to_response(resp)?;
64+
65+
let note = timeout(
66+
DEFAULT_READ_TIMEOUT,
67+
mcp.read_stream_until_notification_message("account/updated"),
68+
)
69+
.await??;
70+
let parsed: ServerNotification = note.try_into()?;
71+
let ServerNotification::AccountUpdated(payload) = parsed else {
72+
bail!("unexpected notification: {parsed:?}");
73+
};
74+
assert!(
75+
payload.auth_method.is_none(),
76+
"auth_method should be None after logout"
77+
);
78+
79+
assert!(
80+
!codex_home.path().join("auth.json").exists(),
81+
"auth.json should be deleted"
82+
);
83+
84+
let status_id = mcp
85+
.send_get_auth_status_request(GetAuthStatusParams {
86+
include_token: Some(true),
87+
refresh_token: Some(false),
88+
})
89+
.await?;
90+
let status_resp: JSONRPCResponse = timeout(
91+
DEFAULT_READ_TIMEOUT,
92+
mcp.read_stream_until_response_message(RequestId::Integer(status_id)),
93+
)
94+
.await??;
95+
let status: GetAuthStatusResponse = to_response(status_resp)?;
96+
assert_eq!(status.auth_method, None);
97+
assert_eq!(status.auth_token, None);
98+
Ok(())
99+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// v2 test suite modules
2+
mod account;

0 commit comments

Comments
 (0)