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
This commit is contained in:
acrognale-oai authored and copyberry committed 2026-09-14 22:31:32 +00:00
1 parent b44af92ca0
commit a4354e2d27
32 files changed
+1547 -120

No files matched your search

@@ -15,6 +15,24 @@ use codex_login::token_data::parse_chatgpt_jwt_claims;
use codex_protocol::auth::AuthMode;
use serde_json::json;
pub async fn mount_workspace_routing(server: &wiremock::MockServer) {
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/backend-api/wham/accounts/check"))
.respond_with(|request: &wiremock::Request| {
let account_id = request
.headers
.get("chatgpt-account-id")
.and_then(|value| value.to_str().ok())
.unwrap_or("account-123");
wiremock::ResponseTemplate::new(200).set_body_json(json!({
"accounts": [{"id": account_id, "workspace_backend_origin": "https://chatgpt.com",
"account_routing_override": "NO_CONSTRAINT"}],
}))
})
.mount(server)
.await;
}
/// Builder for writing a fake ChatGPT auth.json in tests.
#[derive(Debug, Clone)]
pub struct ChatGptAuthFixture {
+1
View File
@@ -16,6 +16,7 @@ pub use analytics_server::start_analytics_events_server;
pub use auth_fixtures::ChatGptAuthFixture;
pub use auth_fixtures::ChatGptIdTokenClaims;
pub use auth_fixtures::encode_id_token;
pub use auth_fixtures::mount_workspace_routing;
pub use auth_fixtures::write_chatgpt_auth;
use codex_app_server_protocol::JSONRPCResponse;
pub use config::MockResponsesConfig;
@@ -184,6 +184,7 @@ impl TestAppServer {
env_overrides: Vec::new(),
args: vec![DISABLE_PLUGIN_STARTUP_TASKS_ARG.to_string()],
exec_server_delay: None,
mock_chatgpt_backend: false,
}
}
@@ -1856,6 +1857,7 @@ pub struct TestAppServerBuilder {
env_overrides: Vec<(String, Option<String>)>,
args: Vec<String>,
exec_server_delay: Option<Duration>,
mock_chatgpt_backend: bool,
}
enum TestAppServerEnvironment {
@@ -1864,6 +1866,11 @@ enum TestAppServerEnvironment {
}
impl TestAppServerBuilder {
pub fn with_mock_chatgpt_backend(mut self) -> Self {
self.mock_chatgpt_backend = true;
self
}
/// Uses this existing CODEX_HOME instead of a temporary one.
pub fn with_codex_home(mut self, codex_home: &Path) -> Self {
self.codex_home = Some(codex_home.to_path_buf());
@@ -1958,6 +1965,7 @@ impl TestAppServerBuilder {
mut env_overrides,
args,
exec_server_delay,
mock_chatgpt_backend,
} = self;
let (codex_home, owned_codex_home) = match codex_home {
Some(codex_home) => (codex_home, None),
@@ -1969,7 +1977,9 @@ impl TestAppServerBuilder {
)
}
};
let attribution_settings_server = if codex_home.join("auth.json").is_file() {
let attribution_settings_server = if mock_chatgpt_backend
|| codex_home.join("auth.json").is_file()
{
let config_path = codex_home.join("config.toml");
let config = std::fs::read_to_string(&config_path)?;
if config
@@ -1979,6 +1989,12 @@ impl TestAppServerBuilder {
None
} else {
let settings_server = MockServer::start().await;
crate::mount_workspace_routing(&settings_server).await;
Mock::given(method("GET"))
.and(path("/backend-api/wham/config/bundle"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
.mount(&settings_server)
.await;
Mock::given(method("GET"))
.and(path("/backend-api/wham/settings/user"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({