[profile-switcher][rust] -- [1/2] Add app-server account session protocol (#25469)

## Summary

Adds the app-server v2 `accountSession/*` protocol used by the Desktop
profile switcher and the backend account metadata client needed to
populate workspace choices.

This is the protocol layer only. The app-server lifecycle and
consolidated saved-session storage are split into a follow-up PR.

## Rust Stack

1. This PR
2. [openai/codex#25383](https://github.com/openai/codex/pull/25383) adds
app-server session lifecycle behavior and consolidated saved-session
storage.

## Validation

- Generated app-server schema fixtures are included from the existing
generation flow in the lifecycle PR where the routes are registered.
- Did not run tests per requested scope.
This commit is contained in:
dhruvgupta-oai
2026-06-04 01:25:11 +05:30
committed by GitHub
Unverified
parent 57ab4c89e0
commit a2ebe07b39
4 changed files with 172 additions and 0 deletions
@@ -138,6 +138,78 @@ pub struct CancelLoginAccountResponse {
pub status: CancelLoginAccountStatus,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AccountSessionsAddParams {
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub switch_to_added_account: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AccountSessionsListParams {
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub refresh_workspace_metadata: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AccountSessionsLogoutParams {
pub session_id: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AccountSessionsSwitchParams {
pub session_id: String,
pub account_id: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AccountSessionsResponse {
pub active_session_id: Option<String>,
pub sessions: Vec<AccountSession>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AccountSession {
pub session_id: String,
pub email: Option<String>,
pub user_id: Option<String>,
pub display_name: Option<String>,
pub image_url: Option<String>,
pub last_used_at: i64,
pub is_active: bool,
pub selected_workspace_account_id: Option<String>,
pub workspaces: Vec<AccountSessionWorkspace>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AccountSessionWorkspace {
pub account_id: String,
pub name: Option<String>,
pub image_url: Option<String>,
pub kind: Option<AccountSessionWorkspaceKind>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub enum AccountSessionWorkspaceKind {
Personal,
Workspace,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
+11
View File
@@ -1,3 +1,4 @@
use crate::types::AccountsCheckResponse;
use crate::types::CodeTaskDetailsResponse;
use crate::types::ConfigBundleResponse;
use crate::types::PaginatedListTaskListItem;
@@ -302,6 +303,16 @@ impl Client {
Ok(Self::rate_limit_snapshots_from_payload(payload))
}
pub async fn get_accounts_check(&self) -> Result<AccountsCheckResponse> {
let url = match self.path_style {
PathStyle::CodexApi => format!("{}/api/codex/accounts/check", self.base_url),
PathStyle::ChatGptApi => format!("{}/wham/accounts/check", self.base_url),
};
let req = self.http.get(&url).headers(self.headers());
let (body, ct) = self.exec_request(req, "GET", &url).await?;
self.decode_json(&url, &ct, &body)
}
pub async fn send_add_credits_nudge_email(
&self,
credit_type: AddCreditsNudgeCreditType,
+2
View File
@@ -4,6 +4,8 @@ pub(crate) mod types;
pub use client::AddCreditsNudgeCreditType;
pub use client::Client;
pub use client::RequestError;
pub use types::AccountEntry;
pub use types::AccountsCheckResponse;
pub use types::CodeTaskDetailsResponse;
pub use types::CodeTaskDetailsResponseExt;
pub use types::ConfigBundleResponse;
+87
View File
@@ -17,6 +17,93 @@ use serde::de::Deserializer;
use serde_json::Value;
use std::collections::HashMap;
#[derive(Clone, Debug)]
pub struct AccountsCheckResponse {
pub accounts: Vec<AccountEntry>,
pub account_ordering: Vec<String>,
pub default_account_id: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct AccountEntry {
pub id: String,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub profile_picture_url: Option<String>,
#[serde(default)]
pub structure: String,
}
#[derive(Deserialize)]
struct RawAccountsCheckResponse {
#[serde(default)]
accounts: RawAccounts,
#[serde(default)]
account_ordering: Vec<String>,
#[serde(default)]
default_account_id: Option<String>,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum RawAccounts {
List(Vec<AccountEntry>),
Map(HashMap<String, ChatGptAccountEntry>),
}
impl Default for RawAccounts {
fn default() -> Self {
Self::List(Vec::new())
}
}
#[derive(Deserialize)]
struct ChatGptAccountEntry {
account: ChatGptAccountInfo,
}
#[derive(Deserialize)]
struct ChatGptAccountInfo {
account_id: Option<String>,
#[serde(default)]
name: Option<String>,
#[serde(default)]
profile_picture_url: Option<String>,
#[serde(default)]
structure: String,
}
impl<'de> Deserialize<'de> for AccountsCheckResponse {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let raw = RawAccountsCheckResponse::deserialize(deserializer)?;
let accounts = match raw.accounts {
RawAccounts::List(accounts) => accounts,
RawAccounts::Map(mut accounts) => raw
.account_ordering
.iter()
.filter_map(|account_id| {
let account = accounts.remove(account_id)?.account;
Some(AccountEntry {
id: account.account_id?,
name: account.name,
profile_picture_url: account.profile_picture_url,
structure: account.structure,
})
})
.collect(),
};
Ok(Self {
accounts,
account_ordering: raw.account_ordering,
default_account_id: raw.default_account_id,
})
}
}
/// Hand-rolled models for the Cloud Tasks task-details response.
/// The generated OpenAPI models are pretty bad. This is a half-step
/// towards hand-rolling them.