Files
codex/codex-rs/app-server/src/turn_admission.rs
T
dhruvgupta-oai d257beb16a 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
2026-10-27 17:04:16 +00:00

126 lines
3.5 KiB
Rust

//! Tracks client runtime changes and turn starts admitted before a shutdown drain.
use codex_app_server_protocol::JSONRPCErrorError;
use codex_extension_api::TurnStartAdmission;
use std::sync::Arc;
use std::sync::Mutex;
use tokio::sync::watch;
use crate::error_code::invalid_request;
use crate::error_code::server_draining_error;
#[derive(Debug, Default)]
struct AdmissionState {
closed: bool,
paused: bool,
active: usize,
}
#[derive(Clone, Debug)]
pub(crate) struct TurnAdmission {
state: Arc<Mutex<AdmissionState>>,
active_tx: watch::Sender<usize>,
}
impl Default for TurnAdmission {
fn default() -> Self {
Self {
state: Arc::new(Mutex::new(AdmissionState::default())),
active_tx: watch::channel(0).0,
}
}
}
pub(crate) struct TurnPermit(TurnAdmission);
pub(crate) struct TurnAdmissionPause(TurnAdmission);
impl TurnAdmission {
pub(crate) async fn pause(&self) -> Result<TurnAdmissionPause, JSONRPCErrorError> {
let mut active = self.subscribe_active();
{
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if state.closed || state.paused {
return Err(invalid_request("Runtime changes are already paused"));
}
state.paused = true;
}
let pause = TurnAdmissionPause(self.clone());
tokio::time::timeout(
std::time::Duration::from_secs(/*secs*/ 10),
active.wait_for(|active| *active == 0),
)
.await
.map_err(|_| invalid_request("Work is still starting; retry the workspace switch"))?
.map_err(|_| invalid_request("Turn admission is unavailable"))?;
Ok(pause)
}
pub(crate) fn begin_drain(&self) {
self.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.closed = true;
}
pub(crate) fn subscribe_active(&self) -> watch::Receiver<usize> {
self.active_tx.subscribe()
}
// Admit and close take the same short lock. The permit keeps shutdown from
// finishing while an earlier request is still preparing or submitting work.
pub(crate) fn admit(&self) -> Result<TurnPermit, JSONRPCErrorError> {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if state.closed {
return Err(server_draining_error());
}
if state.paused {
return Err(invalid_request(
"Workspace change in progress; retry when it completes",
));
}
state.active += 1;
self.active_tx.send_replace(state.active);
Ok(TurnPermit(self.clone()))
}
}
impl Drop for TurnAdmissionPause {
fn drop(&mut self) {
self.0
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.paused = false;
}
}
impl TurnStartAdmission for TurnAdmission {
fn admit_turn_start(&self) -> Option<Box<dyn Send>> {
self.admit()
.ok()
.map(|permit| Box::new(permit) as Box<dyn Send>)
}
}
impl Drop for TurnPermit {
fn drop(&mut self) {
let mut state = self
.0
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.active -= 1;
self.0.active_tx.send_replace(state.active);
}
}
#[cfg(test)]
#[path = "turn_admission_tests.rs"]
mod tests;