Add experimental ChatGPT workspace switching to app-server (#57514)

## What changed

- Add `accountSession/list` and `accountSession/switch` for the active managed ChatGPT login, with optional workspace membership refresh.
- Validate membership and workspace policy, pause new turns, and unload running threads before switching credentials. Notify clients with `thread/closed` and `account/updated`; selecting the current workspace leaves threads running.
- Support browser sign-in for SSO through `allowBrowserLogin`, binding the callback to the requesting login and workspace. Require a separate sign-in for government workspaces.
- Preserve the selected workspace across token refresh and conditionally restore the previous workspace if routing validation fails. Guard credential writes against concurrent login or logout changes.

## Testing

Add app-server and auth tests covering workspace switching, credential preservation, policy and identity rejection, conditional rollback, stale sessions, and SSO callback validation.

GitOrigin-RevId: 1234abc33b71e69b66318da3e201fccc08867906
This commit is contained in:
dhruvgupta-oai
2026-10-27 17:04:16 +00:00
committed by copyberry
parent 27ace641fe
commit d257beb16a
30 changed files with 2050 additions and 22 deletions
@@ -1358,6 +1358,22 @@ client_request_definitions! {
response: v2::CancelLoginAccountResponse,
},
#[experimental("accountSession/list")]
AccountSessionsList => "accountSession/list" {
params: v2::AccountSessionsListParams,
serialization: global("account-auth"),
manual_payload_conversion: manual,
response: v2::AccountSessionsResponse,
},
#[experimental("accountSession/switch")]
AccountSessionsSwitch => "accountSession/switch" {
params: v2::AccountSessionsSwitchParams,
serialization: global("account-auth"),
manual_payload_conversion: manual,
response: v2::AccountSessionsSwitchResponse,
},
LogoutAccount => "account/logout" {
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
serialization: global("account-auth"),
@@ -227,8 +227,26 @@ pub struct AccountSessionsLogoutParams {
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AccountSessionsSwitchParams {
/// Opaque active-login identifier returned by accountSession/list.
pub session_id: String,
/// Workspace to select within that login; this is not a user identifier.
pub account_id: String,
/// Allow Core to start browser sign-in if this session needs SSO for the workspace.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
#[ts(optional, as = "Option<bool>")]
pub allow_browser_login: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(untagged, rename_all_fields = "camelCase")]
#[ts(untagged, rename_all_fields = "camelCase", export_to = "v2/")]
pub enum AccountSessionsSwitchResponse {
Switched {
#[serde(flatten)]
sessions: AccountSessionsResponse,
},
/// Open the URL and await account/login/completed for this login ID.
BrowserLogin { login_id: String, auth_url: String },
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]