Files
codex/codex-rs/app-server/src/account_notifications.rs
T
acrognale-oai a4354e2d27 Expose selected workspace routing in app-server account reads (#45529)
## What changed

- Add experimental `account/read.workspaceRouting` metadata containing the selected ChatGPT workspace ID, resolved HTTPS backend origin, and routing override (`us`, `us_cr`, or `NO_CONSTRAINT`).
- Discover and cache routing through `accounts/check` for saved logins, new logins, and workspace switches. Return `null` for signed-out accounts, API-only accounts, and saved credentials without a selected workspace.
- Validate discovered origins against required `chatgpt_base_url` origins. Return errors for failed or malformed discovery and retry on later reads.
- Wait for requirements and routing before publishing account updates, including to newly initialized connections. Clear routing on logout, discard stale discovery results, and guard queued notifications against account changes.

## Testing

Add unit and integration coverage for origin resolution and validation, discovery failures and retries, startup discovery, workspace switching, logout, configuration changes during discovery, and authentication changes while notifications wait for queue capacity.

GitOrigin-RevId: 2c5367bb01dd7543b08d374f44444323e40f1981
2026-09-14 22:31:32 +00:00

58 lines
2.1 KiB
Rust

//! Publishes account snapshots only while their authentication owner is current.
//! Superseded login attempts still receive a terminal completion notification.
use super::*;
use codex_app_server_protocol::AccountLoginCompletedNotification;
use codex_app_server_protocol::AccountUpdatedNotification;
use codex_login::AuthChangeState;
use tokio::sync::watch;
pub(crate) enum AccountNotification {
LoginCompleted(AccountLoginCompletedNotification),
Updated(AccountUpdatedNotification),
}
impl OutgoingMessageSender {
pub(crate) async fn send_account_notification(
&self,
connection_id: Option<ConnectionId>,
auth_changes: &watch::Receiver<AuthChangeState>,
owner_generation: u64,
notification: AccountNotification,
) {
let Ok(permit) = self.sender.reserve().await else {
return;
};
// Keep the revision read lock through enqueue; never wait while holding it.
let current_auth = auth_changes.borrow();
let notification = match notification {
AccountNotification::LoginCompleted(mut payload) => {
if payload.success && current_auth.owner_generation != owner_generation {
payload.success = false;
payload.error = Some("account changed before sign-in completed".into());
}
ServerNotification::AccountLoginCompleted(payload)
}
AccountNotification::Updated(payload) => {
if current_auth.owner_generation != owner_generation {
return;
}
ServerNotification::AccountUpdated(payload)
}
};
let message = timestamped_server_notification(notification);
permit.send(match connection_id {
Some(connection_id) => OutgoingEnvelope::ToConnection {
connection_id,
message,
write_complete_tx: None,
},
None => OutgoingEnvelope::Broadcast { message },
});
}
}
#[cfg(test)]
#[path = "account_notifications_tests.rs"]
mod tests;