From 8721807c716c4e58880bb720261646b7219183e9 Mon Sep 17 00:00:00 2001 From: shijie-openai Date: Tue, 14 Apr 2026 09:22:54 -0400 Subject: [PATCH 1/7] Preserve ChatGPT HTTP cookies --- MODULE.bazel.lock | 5 + codex-rs/Cargo.lock | 56 ++++++++ codex-rs/Cargo.toml | 2 +- codex-rs/backend-client/src/client.rs | 5 +- codex-rs/codex-client/src/chatgpt_cookies.rs | 129 +++++++++++++++++++ codex-rs/codex-client/src/lib.rs | 2 + codex-rs/login/src/auth/default_client.rs | 12 +- 7 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 codex-rs/codex-client/src/chatgpt_cookies.rs diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index c107884e1dfa..6bc764449615 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -756,6 +756,8 @@ "convert_case_0.10.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"unicode-segmentation\",\"req\":\"^1.9.0\"}],\"features\":{}}", "convert_case_0.6.0": "{\"dependencies\":[{\"name\":\"rand\",\"optional\":true,\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"strum\",\"req\":\"^0.18.0\"},{\"kind\":\"dev\",\"name\":\"strum_macros\",\"req\":\"^0.18.0\"},{\"name\":\"unicode-segmentation\",\"req\":\"^1.9.0\"}],\"features\":{\"random\":[\"rand\"]}}", "cookie-factory_0.3.3": "{\"dependencies\":[{\"features\":[\"attributes\"],\"kind\":\"dev\",\"name\":\"async-std\",\"req\":\"^1.9.0\"},{\"name\":\"futures\",\"optional\":true,\"req\":\"^0.3.16\"},{\"kind\":\"dev\",\"name\":\"maplit\",\"req\":\"^1.0\"}],\"features\":{\"async\":[\"futures\"],\"default\":[\"std\",\"async\"],\"std\":[]}}", + "cookie_0.18.1": "{\"dependencies\":[{\"name\":\"aes-gcm\",\"optional\":true,\"req\":\"^0.10.0\"},{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22\"},{\"name\":\"hkdf\",\"optional\":true,\"req\":\"^0.12.0\"},{\"name\":\"hmac\",\"optional\":true,\"req\":\"^0.12.0\"},{\"name\":\"percent-encoding\",\"optional\":true,\"req\":\"^2.0\"},{\"name\":\"rand\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10.0\"},{\"name\":\"subtle\",\"optional\":true,\"req\":\"^2.3\"},{\"default_features\":false,\"features\":[\"std\",\"parsing\",\"formatting\",\"macros\"],\"name\":\"time\",\"req\":\"^0.3\"},{\"kind\":\"build\",\"name\":\"version_check\",\"req\":\"^0.9.4\"}],\"features\":{\"key-expansion\":[\"sha2\",\"hkdf\"],\"percent-encode\":[\"percent-encoding\"],\"private\":[\"aes-gcm\",\"base64\",\"rand\",\"subtle\"],\"secure\":[\"private\",\"signed\",\"key-expansion\"],\"signed\":[\"hmac\",\"sha2\",\"base64\",\"rand\",\"subtle\"]}}", + "cookie_store_0.22.1": "{\"dependencies\":[{\"features\":[\"percent-encode\"],\"name\":\"cookie\",\"req\":\"^0.18.0\"},{\"name\":\"document-features\",\"req\":\"^0.2.10\"},{\"name\":\"idna\",\"req\":\"^1.0\"},{\"name\":\"indexmap\",\"optional\":true,\"req\":\"^2.6.0\"},{\"name\":\"log\",\"req\":\"^0.4.17\"},{\"name\":\"publicsuffix\",\"optional\":true,\"req\":\"^2.2.3\"},{\"name\":\"ron\",\"optional\":true,\"req\":\"^0.10.1\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.147\"},{\"name\":\"serde_derive\",\"optional\":true,\"req\":\"^1.0.147\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.87\"},{\"name\":\"time\",\"req\":\"^0.3.47\"},{\"name\":\"url\",\"req\":\"^2.3.1\"}],\"features\":{\"default\":[\"public_suffix\",\"serde_json\"],\"log_secure_cookie_values\":[],\"preserve_order\":[\"dep:indexmap\"],\"public_suffix\":[\"dep:publicsuffix\"],\"serde\":[\"dep:serde\",\"dep:serde_derive\"],\"serde_json\":[\"serde\",\"dep:serde_json\"],\"serde_ron\":[\"serde\",\"dep:ron\"],\"wasm-bindgen\":[\"time/wasm-bindgen\"]}}", "core-foundation-sys_0.8.7": "{\"dependencies\":[],\"features\":{\"default\":[\"link\"],\"link\":[],\"mac_os_10_7_support\":[],\"mac_os_10_8_features\":[]}}", "core-foundation_0.10.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"core-foundation-sys\",\"req\":\"^0.8\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"default\":[\"link\"],\"link\":[\"core-foundation-sys/link\"],\"mac_os_10_7_support\":[\"core-foundation-sys/mac_os_10_7_support\"],\"mac_os_10_8_features\":[\"core-foundation-sys/mac_os_10_8_features\"],\"with-uuid\":[\"dep:uuid\"]}}", "core-foundation_0.9.4": "{\"dependencies\":[{\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"core-foundation-sys\",\"req\":\"^0.8.6\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^0.5\"}],\"features\":{\"default\":[\"link\"],\"link\":[\"core-foundation-sys/link\"],\"mac_os_10_7_support\":[\"core-foundation-sys/mac_os_10_7_support\"],\"mac_os_10_8_features\":[\"core-foundation-sys/mac_os_10_8_features\"],\"with-chrono\":[\"chrono\"],\"with-uuid\":[\"uuid\"]}}", @@ -836,6 +838,7 @@ "display_container_0.9.0": "{\"dependencies\":[{\"name\":\"either\",\"req\":\"^1.8\"},{\"name\":\"indenter\",\"req\":\"^0.3.3\"}],\"features\":{}}", "displaydoc_0.2.5": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^0.6.1\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1\"},{\"name\":\"syn\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"thiserror\",\"req\":\"^1.0.24\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "dns-lookup_3.0.1": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"name\":\"socket2\",\"req\":\"^0.6.0\"},{\"features\":[\"Win32_Networking_WinSock\",\"Win32_Foundation\"],\"name\":\"windows-sys\",\"req\":\"^0.60\",\"target\":\"cfg(windows)\"}],\"features\":{}}", + "document-features_0.2.12": "{\"dependencies\":[{\"name\":\"litrs\",\"req\":\"^1.0.0\"}],\"features\":{\"default\":[],\"self-test\":[]}}", "dotenvy_0.15.7": "{\"dependencies\":[{\"name\":\"clap\",\"optional\":true,\"req\":\"^3.2\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.16.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.3.0\"}],\"features\":{\"cli\":[\"clap\"]}}", "downcast-rs_1.2.1": "{\"dependencies\":[],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "dtor-proc-macro_0.0.6": "{\"dependencies\":[],\"features\":{\"default\":[]}}", @@ -1145,6 +1148,7 @@ "linux-raw-sys_0.12.1": "{\"dependencies\":[{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.100\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1.0\"}],\"features\":{\"auxvec\":[],\"bootparam\":[],\"btrfs\":[],\"default\":[\"std\",\"general\",\"errno\"],\"elf\":[],\"elf_uapi\":[],\"errno\":[],\"general\":[],\"if_arp\":[],\"if_ether\":[],\"if_packet\":[],\"if_tun\":[],\"image\":[],\"io_uring\":[],\"ioctl\":[],\"landlock\":[],\"loop_device\":[],\"mempolicy\":[],\"net\":[],\"netlink\":[],\"no_std\":[],\"prctl\":[],\"ptrace\":[],\"rustc-dep-of-std\":[\"core\",\"no_std\"],\"std\":[],\"system\":[],\"vm_sockets\":[],\"xdp\":[]}}", "linux-raw-sys_0.4.15": "{\"dependencies\":[{\"name\":\"compiler_builtins\",\"optional\":true,\"req\":\"^0.1.49\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.100\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1.0\"}],\"features\":{\"bootparam\":[],\"btrfs\":[],\"default\":[\"std\",\"general\",\"errno\"],\"elf\":[],\"elf_uapi\":[],\"errno\":[],\"general\":[],\"if_arp\":[],\"if_ether\":[],\"if_packet\":[],\"io_uring\":[],\"ioctl\":[],\"landlock\":[],\"loop_device\":[],\"mempolicy\":[],\"net\":[],\"netlink\":[],\"no_std\":[],\"prctl\":[],\"ptrace\":[],\"rustc-dep-of-std\":[\"core\",\"compiler_builtins\",\"no_std\"],\"std\":[],\"system\":[],\"xdp\":[]}}", "litemap_0.8.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3.1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"name\":\"databake\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"features\":[\"use-std\"],\"kind\":\"dev\",\"name\":\"postcard\",\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"features\":[\"validation\"],\"kind\":\"dev\",\"name\":\"rkyv\",\"req\":\"^0.7\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.220\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"serde_core\",\"req\":\"^1.0.220\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.45\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"yoke\",\"optional\":true,\"req\":\"^0.8.0\"}],\"features\":{\"alloc\":[],\"databake\":[\"dep:databake\"],\"default\":[\"alloc\"],\"serde\":[\"dep:serde_core\",\"alloc\"],\"testing\":[\"alloc\"],\"yoke\":[\"dep:yoke\"]}}", + "litrs_1.0.0": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"optional\":true,\"req\":\"^1.0.63\"},{\"name\":\"unicode-xid\",\"optional\":true,\"req\":\"^0.2.4\"}],\"features\":{\"check_suffix\":[\"unicode-xid\"]}}", "local-waker_0.1.4": "{\"dependencies\":[],\"features\":{}}", "lock_api_0.4.14": "{\"dependencies\":[{\"name\":\"owning_ref\",\"optional\":true,\"req\":\"^0.4.1\"},{\"default_features\":false,\"name\":\"scopeguard\",\"req\":\"^1.1.0\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.126\"}],\"features\":{\"arc_lock\":[],\"atomic_usize\":[],\"default\":[\"atomic_usize\"],\"nightly\":[]}}", "log_0.4.29": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"proc-macro2\",\"req\":\"^1.0.63\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"sval\",\"optional\":true,\"req\":\"^2.16\"},{\"kind\":\"dev\",\"name\":\"sval\",\"req\":\"^2.16\"},{\"kind\":\"dev\",\"name\":\"sval_derive\",\"req\":\"^2.16\"},{\"default_features\":false,\"name\":\"sval_ref\",\"optional\":true,\"req\":\"^2.16\"},{\"default_features\":false,\"features\":[\"inline-i128\"],\"name\":\"value-bag\",\"optional\":true,\"req\":\"^1.12\"},{\"features\":[\"test\"],\"kind\":\"dev\",\"name\":\"value-bag\",\"req\":\"^1.12\"}],\"features\":{\"kv\":[],\"kv_serde\":[\"kv_std\",\"value-bag/serde\",\"serde\"],\"kv_std\":[\"std\",\"kv\",\"value-bag/error\"],\"kv_sval\":[\"kv\",\"value-bag/sval\",\"sval\",\"sval_ref\"],\"kv_unstable\":[\"kv\",\"value-bag\"],\"kv_unstable_serde\":[\"kv_serde\",\"kv_unstable_std\"],\"kv_unstable_std\":[\"kv_std\",\"kv_unstable\"],\"kv_unstable_sval\":[\"kv_sval\",\"kv_unstable\"],\"max_level_debug\":[],\"max_level_error\":[],\"max_level_info\":[],\"max_level_off\":[],\"max_level_trace\":[],\"max_level_warn\":[],\"release_max_level_debug\":[],\"release_max_level_error\":[],\"release_max_level_info\":[],\"release_max_level_off\":[],\"release_max_level_trace\":[],\"release_max_level_warn\":[],\"serde\":[\"serde_core\"],\"std\":[]}}", @@ -1329,6 +1333,7 @@ "protoc-gen-tonic_0.4.1": "{\"dependencies\":[{\"name\":\"heck\",\"req\":\"^0.5.0\"},{\"name\":\"prettyplease\",\"req\":\"^0.2.9\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"prost\",\"req\":\"^0.13.1\"},{\"default_features\":false,\"name\":\"prost-build\",\"req\":\"^0.13.1\"},{\"default_features\":false,\"name\":\"prost-types\",\"req\":\"^0.13.1\"},{\"name\":\"protoc-gen-prost\",\"req\":\"^0.4.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"regex\",\"req\":\"^1.5.5\"},{\"features\":[\"parsing\",\"full\"],\"name\":\"syn\",\"req\":\"^2.0.22\"},{\"name\":\"tonic-build\",\"req\":\"^0.12.0\"}],\"features\":{}}", "psl-types_2.0.11": "{\"dependencies\":[],\"features\":{}}", "psl_2.1.184": "{\"dependencies\":[{\"name\":\"psl-types\",\"req\":\"^2.0.11\"},{\"kind\":\"dev\",\"name\":\"rspec\",\"req\":\"^1.0.0\"}],\"features\":{\"default\":[\"helpers\"],\"helpers\":[]}}", + "publicsuffix_2.3.0": "{\"dependencies\":[{\"features\":[\"inline-more\"],\"name\":\"hashbrown\",\"optional\":true,\"req\":\"^0.15.1\"},{\"name\":\"idna\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"psl-types\",\"req\":\"^2.0.11\"},{\"kind\":\"dev\",\"name\":\"rspec\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"name\":\"unicase\",\"optional\":true,\"req\":\"^2.6.0\"}],\"features\":{\"anycase\":[\"unicase\"],\"default\":[\"punycode\"],\"punycode\":[\"idna\"],\"std\":[]}}", "pulldown-cmark-escape_0.10.1": "{\"dependencies\":[],\"features\":{\"simd\":[]}}", "pulldown-cmark_0.10.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3.1\"},{\"name\":\"bitflags\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"getopts\",\"optional\":true,\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.4\"},{\"name\":\"memchr\",\"req\":\"^2.5\"},{\"name\":\"pulldown-cmark-escape\",\"optional\":true,\"req\":\"^0.10.0\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.6\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.61\"},{\"name\":\"unicase\",\"req\":\"^2.6\"}],\"features\":{\"default\":[\"getopts\",\"html\"],\"gen-tests\":[],\"html\":[\"pulldown-cmark-escape\"],\"simd\":[\"pulldown-cmark-escape?/simd\"]}}", "pxfm_0.1.27": "{\"dependencies\":[{\"name\":\"num-traits\",\"req\":\"^0.2.3\"}],\"features\":{}}", diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 25c12ac154c3..158d7fff5e47 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -3495,6 +3495,17 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "cookie-factory" version = "0.3.3" @@ -3504,6 +3515,24 @@ dependencies = [ "futures", ] +[[package]] +name = "cookie_store" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -4390,6 +4419,15 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -7502,6 +7540,12 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "livekit-protocol" version = "0.7.1" @@ -9307,6 +9351,16 @@ version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + [[package]] name = "pulldown-cmark" version = "0.10.3" @@ -9991,6 +10045,8 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", + "cookie", + "cookie_store", "encoding_rs", "futures-channel", "futures-core", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 236412051177..1abeadbd8e04 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -295,7 +295,7 @@ ratatui = "0.29.0" ratatui-macros = "0.6.0" regex = "1.12.3" regex-lite = "0.1.8" -reqwest = "0.12" +reqwest = { version = "0.12", features = ["cookies"] } rmcp = { version = "0.15.0", default-features = false } runfiles = { git = "https://github.com/dzbarsky/rules_rust", rev = "b56cbaa8465e74127f1ea216f813cd377295ad81" } rustls = { version = "0.23", default-features = false, features = [ diff --git a/codex-rs/backend-client/src/client.rs b/codex-rs/backend-client/src/client.rs index e6ea0253b896..8acb6e97b709 100644 --- a/codex-rs/backend-client/src/client.rs +++ b/codex-rs/backend-client/src/client.rs @@ -6,6 +6,7 @@ use crate::types::RateLimitStatusPayload; use crate::types::TurnAttemptsSiblingTurnsResponse; use anyhow::Result; use codex_client::build_reqwest_client_with_custom_ca; +use codex_client::with_chatgpt_cookie_store; use codex_login::CodexAuth; use codex_login::default_client::get_codex_user_agent; use codex_protocol::account::PlanType as AccountPlanType; @@ -137,7 +138,9 @@ impl Client { { base_url = format!("{base_url}/backend-api"); } - let http = build_reqwest_client_with_custom_ca(reqwest::Client::builder())?; + let http = build_reqwest_client_with_custom_ca(with_chatgpt_cookie_store( + reqwest::Client::builder(), + ))?; let path_style = PathStyle::from_base_url(&base_url); Ok(Self { base_url, diff --git a/codex-rs/codex-client/src/chatgpt_cookies.rs b/codex-rs/codex-client/src/chatgpt_cookies.rs new file mode 100644 index 000000000000..8c56af40939a --- /dev/null +++ b/codex-rs/codex-client/src/chatgpt_cookies.rs @@ -0,0 +1,129 @@ +use std::sync::Arc; +use std::sync::LazyLock; + +use reqwest::cookie::CookieStore; +use reqwest::cookie::Jar; +use reqwest::header::HeaderValue; + +static SHARED_CHATGPT_COOKIE_STORE: LazyLock> = + LazyLock::new(|| Arc::new(ChatGptCookieStore::default())); + +#[derive(Debug, Default)] +struct ChatGptCookieStore { + jar: Jar, +} + +impl CookieStore for ChatGptCookieStore { + fn set_cookies( + &self, + cookie_headers: &mut dyn Iterator, + url: &reqwest::Url, + ) { + if is_chatgpt_cookie_url(url) { + self.jar.set_cookies(cookie_headers, url); + } + } + + fn cookies(&self, url: &reqwest::Url) -> Option { + if is_chatgpt_cookie_url(url) { + self.jar.cookies(url) + } else { + None + } + } +} + +/// Adds the process-local ChatGPT cookie jar used by Codex HTTP clients. +/// +/// The jar is intentionally not persisted to disk. It only preserves cookies for ChatGPT backend +/// hosts so Cloudflare visitor cookies can be replayed across freshly built HTTP clients without +/// broadening cookie handling for arbitrary third-party hosts. +pub fn with_chatgpt_cookie_store(builder: reqwest::ClientBuilder) -> reqwest::ClientBuilder { + builder.cookie_provider(Arc::clone(&SHARED_CHATGPT_COOKIE_STORE)) +} + +fn is_chatgpt_cookie_url(url: &reqwest::Url) -> bool { + match url.scheme() { + "http" | "https" => {} + _ => return false, + } + + let Some(host) = url.host_str() else { + return false; + }; + + host == "chatgpt.com" + || host.ends_with(".chatgpt.com") + || host == "chat.openai.com" + || host == "chatgpt-staging.com" + || host.ends_with(".chatgpt-staging.com") +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + use reqwest::cookie::CookieStore; + + #[test] + fn stores_and_returns_chatgpt_cookies() { + let store = ChatGptCookieStore::default(); + let url = reqwest::Url::parse("https://chatgpt.com/backend-api/codex/responses").unwrap(); + let set_cookie = HeaderValue::from_static("_cfuvid=visitor; Path=/; Secure; HttpOnly"); + + store.set_cookies(&mut std::iter::once(&set_cookie), &url); + + assert_eq!( + store + .cookies(&url) + .and_then(|value| value.to_str().ok().map(str::to_string)), + Some("_cfuvid=visitor".to_string()) + ); + } + + #[test] + fn ignores_non_chatgpt_cookies() { + let store = ChatGptCookieStore::default(); + let url = reqwest::Url::parse("https://api.openai.com/v1/responses").unwrap(); + let set_cookie = HeaderValue::from_static("_cfuvid=visitor; Path=/; Secure; HttpOnly"); + + store.set_cookies(&mut std::iter::once(&set_cookie), &url); + + assert_eq!(store.cookies(&url), None); + } + + #[test] + fn does_not_return_chatgpt_cookies_for_other_hosts() { + let store = ChatGptCookieStore::default(); + let chatgpt_url = + reqwest::Url::parse("https://chatgpt.com/backend-api/codex/responses").unwrap(); + let api_url = reqwest::Url::parse("https://api.openai.com/v1/responses").unwrap(); + let set_cookie = HeaderValue::from_static("_cfuvid=visitor; Path=/; Secure; HttpOnly"); + + store.set_cookies(&mut std::iter::once(&set_cookie), &chatgpt_url); + + assert_eq!(store.cookies(&api_url), None); + } + + #[test] + fn recognizes_chatgpt_hosts_without_suffix_tricks() { + for url in [ + "https://chatgpt.com/backend-api/codex/responses", + "https://foo.chatgpt.com/backend-api/codex/responses", + "https://chat.openai.com/backend-api/codex/responses", + "https://api.chatgpt-staging.com/backend-api/codex/responses", + ] { + let url = reqwest::Url::parse(url).unwrap(); + assert!(is_chatgpt_cookie_url(&url)); + } + + for url in [ + "https://evilchatgpt.com/backend-api/codex/responses", + "https://chatgpt.com.evil.example/backend-api/codex/responses", + "https://api.openai.com/v1/responses", + ] { + let url = reqwest::Url::parse(url).unwrap(); + assert!(!is_chatgpt_cookie_url(&url)); + } + } +} diff --git a/codex-rs/codex-client/src/lib.rs b/codex-rs/codex-client/src/lib.rs index 7b1bf84753e2..8e7fab8acf15 100644 --- a/codex-rs/codex-client/src/lib.rs +++ b/codex-rs/codex-client/src/lib.rs @@ -1,3 +1,4 @@ +mod chatgpt_cookies; mod custom_ca; mod default_client; mod error; @@ -7,6 +8,7 @@ mod sse; mod telemetry; mod transport; +pub use crate::chatgpt_cookies::with_chatgpt_cookie_store; pub use crate::custom_ca::BuildCustomCaTransportError; /// Test-only subprocess hook for custom CA coverage. /// diff --git a/codex-rs/login/src/auth/default_client.rs b/codex-rs/login/src/auth/default_client.rs index 7923c7478205..e05d86051bad 100644 --- a/codex-rs/login/src/auth/default_client.rs +++ b/codex-rs/login/src/auth/default_client.rs @@ -8,6 +8,7 @@ use codex_client::BuildCustomCaTransportError; use codex_client::CodexHttpClient; pub use codex_client::CodexRequestBuilder; use codex_client::build_reqwest_client_with_custom_ca; +use codex_client::with_chatgpt_cookie_store; use codex_terminal_detection::user_agent; use reqwest::header::HeaderMap; use reqwest::header::HeaderValue; @@ -201,7 +202,15 @@ pub fn create_client() -> CodexHttpClient { pub fn build_reqwest_client() -> reqwest::Client { try_build_reqwest_client().unwrap_or_else(|error| { tracing::warn!(error = %error, "failed to build default reqwest client"); - reqwest::Client::new() + with_chatgpt_cookie_store(reqwest::Client::builder()) + .build() + .unwrap_or_else(|fallback_error| { + tracing::warn!( + error = %fallback_error, + "failed to build fallback reqwest client with ChatGPT cookie store" + ); + reqwest::Client::new() + }) }) } @@ -219,6 +228,7 @@ pub fn try_build_reqwest_client() -> Result Date: Tue, 14 Apr 2026 11:47:09 -0400 Subject: [PATCH 2/7] Share ChatGPT host allowlist --- codex-rs/Cargo.lock | 1 + codex-rs/app-server/Cargo.toml | 1 + .../src/transport/remote_control/protocol.rs | 24 ++++++++----- codex-rs/codex-client/src/chatgpt_cookies.rs | 29 ++++----------- codex-rs/codex-client/src/chatgpt_hosts.rs | 36 +++++++++++++++++++ codex-rs/codex-client/src/lib.rs | 2 ++ 6 files changed, 62 insertions(+), 31 deletions(-) create mode 100644 codex-rs/codex-client/src/chatgpt_hosts.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 158d7fff5e47..8f59f809df27 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1468,6 +1468,7 @@ dependencies = [ "codex-arg0", "codex-backend-client", "codex-chatgpt", + "codex-client", "codex-cloud-requirements", "codex-config", "codex-core", diff --git a/codex-rs/app-server/Cargo.toml b/codex-rs/app-server/Cargo.toml index ea49058e6cf5..4c119f4136e6 100644 --- a/codex-rs/app-server/Cargo.toml +++ b/codex-rs/app-server/Cargo.toml @@ -45,6 +45,7 @@ codex-utils-pty = { workspace = true } codex-backend-client = { workspace = true } codex-file-search = { workspace = true } codex-chatgpt = { workspace = true } +codex-client = { workspace = true } codex-login = { workspace = true } codex-mcp = { workspace = true } codex-models-manager = { workspace = true } diff --git a/codex-rs/app-server/src/transport/remote_control/protocol.rs b/codex-rs/app-server/src/transport/remote_control/protocol.rs index dcb79e5c359e..e639d34a4c64 100644 --- a/codex-rs/app-server/src/transport/remote_control/protocol.rs +++ b/codex-rs/app-server/src/transport/remote_control/protocol.rs @@ -1,5 +1,6 @@ use crate::outgoing_message::OutgoingMessage; use codex_app_server_protocol::JSONRPCMessage; +use codex_client::is_allowed_chatgpt_host; use serde::Deserialize; use serde::Serialize; use std::io; @@ -105,14 +106,11 @@ pub(crate) struct ServerEnvelope { pub(crate) seq_id: u64, } -fn is_allowed_chatgpt_host(host: &Option>) -> bool { +fn is_allowed_remote_control_chatgpt_host(host: &Option>) -> bool { let Some(Host::Domain(host)) = *host else { return false; }; - host == "chatgpt.com" - || host == "chatgpt-staging.com" - || host.ends_with(".chatgpt.com") - || host.ends_with(".chatgpt-staging.com") + is_allowed_chatgpt_host(host) } fn is_localhost(host: &Option>) -> bool { @@ -137,7 +135,7 @@ pub(super) fn normalize_remote_control_url( io::Error::new( ErrorKind::InvalidInput, format!( - "invalid remote control URL `{remote_control_url}`; expected HTTPS URL for chatgpt.com or chatgpt-staging.com, or HTTP/HTTPS URL for localhost" + "invalid remote control URL `{remote_control_url}`; expected HTTPS URL for a ChatGPT host, or HTTP/HTTPS URL for localhost" ), ) }; @@ -156,7 +154,7 @@ pub(super) fn normalize_remote_control_url( .map_err(map_url_parse_error)?; let host = enroll_url.host(); match enroll_url.scheme() { - "https" if is_localhost(&host) || is_allowed_chatgpt_host(&host) => { + "https" if is_localhost(&host) || is_allowed_remote_control_chatgpt_host(&host) => { websocket_url.set_scheme("wss").map_err(map_scheme_error)?; } "http" if is_localhost(&host) => { @@ -200,6 +198,16 @@ mod tests { .to_string(), } ); + assert_eq!( + normalize_remote_control_url("https://chat.openai.com/backend-api") + .expect("chat.openai.com URL should normalize"), + RemoteControlTarget { + websocket_url: "wss://chat.openai.com/backend-api/wham/remote/control/server" + .to_string(), + enroll_url: "https://chat.openai.com/backend-api/wham/remote/control/server/enroll" + .to_string(), + } + ); } #[test] @@ -243,7 +251,7 @@ mod tests { assert_eq!( err.to_string(), format!( - "invalid remote control URL `{remote_control_url}`; expected HTTPS URL for chatgpt.com or chatgpt-staging.com, or HTTP/HTTPS URL for localhost" + "invalid remote control URL `{remote_control_url}`; expected HTTPS URL for a ChatGPT host, or HTTP/HTTPS URL for localhost" ) ); } diff --git a/codex-rs/codex-client/src/chatgpt_cookies.rs b/codex-rs/codex-client/src/chatgpt_cookies.rs index 8c56af40939a..e1117850cb3a 100644 --- a/codex-rs/codex-client/src/chatgpt_cookies.rs +++ b/codex-rs/codex-client/src/chatgpt_cookies.rs @@ -5,6 +5,8 @@ use reqwest::cookie::CookieStore; use reqwest::cookie::Jar; use reqwest::header::HeaderValue; +use crate::chatgpt_hosts::is_allowed_chatgpt_host; + static SHARED_CHATGPT_COOKIE_STORE: LazyLock> = LazyLock::new(|| Arc::new(ChatGptCookieStore::default())); @@ -52,11 +54,7 @@ fn is_chatgpt_cookie_url(url: &reqwest::Url) -> bool { return false; }; - host == "chatgpt.com" - || host.ends_with(".chatgpt.com") - || host == "chat.openai.com" - || host == "chatgpt-staging.com" - || host.ends_with(".chatgpt-staging.com") + is_allowed_chatgpt_host(host) } #[cfg(test)] @@ -106,24 +104,9 @@ mod tests { } #[test] - fn recognizes_chatgpt_hosts_without_suffix_tricks() { - for url in [ - "https://chatgpt.com/backend-api/codex/responses", - "https://foo.chatgpt.com/backend-api/codex/responses", - "https://chat.openai.com/backend-api/codex/responses", - "https://api.chatgpt-staging.com/backend-api/codex/responses", - ] { - let url = reqwest::Url::parse(url).unwrap(); - assert!(is_chatgpt_cookie_url(&url)); - } + fn only_allows_http_and_https_urls() { + let url = reqwest::Url::parse("wss://chatgpt.com/backend-api/codex/responses").unwrap(); - for url in [ - "https://evilchatgpt.com/backend-api/codex/responses", - "https://chatgpt.com.evil.example/backend-api/codex/responses", - "https://api.openai.com/v1/responses", - ] { - let url = reqwest::Url::parse(url).unwrap(); - assert!(!is_chatgpt_cookie_url(&url)); - } + assert!(!is_chatgpt_cookie_url(&url)); } } diff --git a/codex-rs/codex-client/src/chatgpt_hosts.rs b/codex-rs/codex-client/src/chatgpt_hosts.rs new file mode 100644 index 000000000000..1303618cf1a3 --- /dev/null +++ b/codex-rs/codex-client/src/chatgpt_hosts.rs @@ -0,0 +1,36 @@ +/// Returns whether `host` is one of the ChatGPT hosts Codex is allowed to treat +/// as first-party ChatGPT traffic. +pub fn is_allowed_chatgpt_host(host: &str) -> bool { + host == "chatgpt.com" + || host.ends_with(".chatgpt.com") + || host == "chat.openai.com" + || host == "chatgpt-staging.com" + || host.ends_with(".chatgpt-staging.com") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn recognizes_chatgpt_hosts_without_suffix_tricks() { + for host in [ + "chatgpt.com", + "foo.chatgpt.com", + "chat.openai.com", + "chatgpt-staging.com", + "api.chatgpt-staging.com", + ] { + assert!(is_allowed_chatgpt_host(host)); + } + + for host in [ + "evilchatgpt.com", + "chatgpt.com.evil.example", + "api.openai.com", + "foo.chat.openai.com", + ] { + assert!(!is_allowed_chatgpt_host(host)); + } + } +} diff --git a/codex-rs/codex-client/src/lib.rs b/codex-rs/codex-client/src/lib.rs index 8e7fab8acf15..310ee78eec2f 100644 --- a/codex-rs/codex-client/src/lib.rs +++ b/codex-rs/codex-client/src/lib.rs @@ -1,4 +1,5 @@ mod chatgpt_cookies; +mod chatgpt_hosts; mod custom_ca; mod default_client; mod error; @@ -9,6 +10,7 @@ mod telemetry; mod transport; pub use crate::chatgpt_cookies::with_chatgpt_cookie_store; +pub use crate::chatgpt_hosts::is_allowed_chatgpt_host; pub use crate::custom_ca::BuildCustomCaTransportError; /// Test-only subprocess hook for custom CA coverage. /// From 31400dca8b45d93275087c19eac247c7fac91982 Mon Sep 17 00:00:00 2001 From: shijie-openai Date: Thu, 16 Apr 2026 01:47:01 -0400 Subject: [PATCH 3/7] Limit shared ChatGPT cookies to Cloudflare --- codex-rs/backend-client/src/client.rs | 4 +- codex-rs/codex-client/src/chatgpt_cookies.rs | 161 ++++++++++++++++--- codex-rs/codex-client/src/lib.rs | 2 +- codex-rs/login/src/auth/default_client.rs | 8 +- 4 files changed, 147 insertions(+), 28 deletions(-) diff --git a/codex-rs/backend-client/src/client.rs b/codex-rs/backend-client/src/client.rs index 8acb6e97b709..d3c9db2bd764 100644 --- a/codex-rs/backend-client/src/client.rs +++ b/codex-rs/backend-client/src/client.rs @@ -6,7 +6,7 @@ use crate::types::RateLimitStatusPayload; use crate::types::TurnAttemptsSiblingTurnsResponse; use anyhow::Result; use codex_client::build_reqwest_client_with_custom_ca; -use codex_client::with_chatgpt_cookie_store; +use codex_client::with_chatgpt_cloudflare_cookie_store; use codex_login::CodexAuth; use codex_login::default_client::get_codex_user_agent; use codex_protocol::account::PlanType as AccountPlanType; @@ -138,7 +138,7 @@ impl Client { { base_url = format!("{base_url}/backend-api"); } - let http = build_reqwest_client_with_custom_ca(with_chatgpt_cookie_store( + let http = build_reqwest_client_with_custom_ca(with_chatgpt_cloudflare_cookie_store( reqwest::Client::builder(), ))?; let path_style = PathStyle::from_base_url(&base_url); diff --git a/codex-rs/codex-client/src/chatgpt_cookies.rs b/codex-rs/codex-client/src/chatgpt_cookies.rs index e1117850cb3a..f40d48c0e315 100644 --- a/codex-rs/codex-client/src/chatgpt_cookies.rs +++ b/codex-rs/codex-client/src/chatgpt_cookies.rs @@ -7,41 +7,52 @@ use reqwest::header::HeaderValue; use crate::chatgpt_hosts::is_allowed_chatgpt_host; -static SHARED_CHATGPT_COOKIE_STORE: LazyLock> = - LazyLock::new(|| Arc::new(ChatGptCookieStore::default())); +// WARNING: this store is process-global and may be shared across auth contexts. +// It must only ever contain Cloudflare infrastructure cookies. Never extend this +// store to persist ChatGPT account, session, auth, or other user-specific cookie +// data. +static SHARED_CHATGPT_CLOUDFLARE_COOKIE_STORE: LazyLock> = + LazyLock::new(|| Arc::new(ChatGptCloudflareCookieStore::default())); #[derive(Debug, Default)] -struct ChatGptCookieStore { +struct ChatGptCloudflareCookieStore { jar: Jar, } -impl CookieStore for ChatGptCookieStore { +impl CookieStore for ChatGptCloudflareCookieStore { fn set_cookies( &self, cookie_headers: &mut dyn Iterator, url: &reqwest::Url, ) { - if is_chatgpt_cookie_url(url) { - self.jar.set_cookies(cookie_headers, url); + if !is_chatgpt_cookie_url(url) { + return; } + + let mut cloudflare_cookie_headers = + cookie_headers.filter(|header| is_allowed_cloudflare_set_cookie_header(header)); + self.jar.set_cookies(&mut cloudflare_cookie_headers, url); } fn cookies(&self, url: &reqwest::Url) -> Option { if is_chatgpt_cookie_url(url) { - self.jar.cookies(url) + self.jar.cookies(url).and_then(only_cloudflare_cookies) } else { None } } } -/// Adds the process-local ChatGPT cookie jar used by Codex HTTP clients. +/// Adds the process-local ChatGPT Cloudflare cookie jar used by Codex HTTP clients. /// -/// The jar is intentionally not persisted to disk. It only preserves cookies for ChatGPT backend -/// hosts so Cloudflare visitor cookies can be replayed across freshly built HTTP clients without -/// broadening cookie handling for arbitrary third-party hosts. -pub fn with_chatgpt_cookie_store(builder: reqwest::ClientBuilder) -> reqwest::ClientBuilder { - builder.cookie_provider(Arc::clone(&SHARED_CHATGPT_COOKIE_STORE)) +/// WARNING: this jar is global within the process. It is only acceptable because it hardcodes a +/// small allowlist of Cloudflare cookie names and refuses all other ChatGPT cookies. Do not store +/// ChatGPT account, session, auth, or other user-specific cookies here. If a future caller needs +/// those cookies, the store must be scoped to the auth/session owner instead of shared globally. +pub fn with_chatgpt_cloudflare_cookie_store( + builder: reqwest::ClientBuilder, +) -> reqwest::ClientBuilder { + builder.cookie_provider(Arc::clone(&SHARED_CHATGPT_CLOUDFLARE_COOKIE_STORE)) } fn is_chatgpt_cookie_url(url: &reqwest::Url) -> bool { @@ -57,6 +68,54 @@ fn is_chatgpt_cookie_url(url: &reqwest::Url) -> bool { is_allowed_chatgpt_host(host) } +fn is_allowed_cloudflare_set_cookie_header(header: &HeaderValue) -> bool { + header + .to_str() + .ok() + .and_then(set_cookie_name) + .is_some_and(is_allowed_cloudflare_cookie_name) +} + +fn set_cookie_name(header: &str) -> Option<&str> { + let (name, _) = header.split_once('=')?; + let name = name.trim(); + (!name.is_empty()).then_some(name) +} + +fn only_cloudflare_cookies(header: HeaderValue) -> Option { + let header = header.to_str().ok()?; + let cookies = header + .split(';') + .filter_map(|cookie| { + let cookie = cookie.trim(); + let name = cookie.split_once('=')?.0.trim(); + is_allowed_cloudflare_cookie_name(name).then_some(cookie) + }) + .collect::>() + .join("; "); + + if cookies.is_empty() { + None + } else { + HeaderValue::from_str(&cookies).ok() + } +} + +fn is_allowed_cloudflare_cookie_name(name: &str) -> bool { + matches!( + name, + "__cf_bm" + | "__cflb" + | "__cfruid" + | "__cfseq" + | "__cfwaitingroom" + | "_cfuvid" + | "cf_clearance" + | "cf_ob_info" + | "cf_use_ob" + ) || name.starts_with("cf_chl_") +} + #[cfg(test)] mod tests { use super::*; @@ -64,24 +123,26 @@ mod tests { use reqwest::cookie::CookieStore; #[test] - fn stores_and_returns_chatgpt_cookies() { - let store = ChatGptCookieStore::default(); + fn stores_and_returns_cloudflare_cookies_for_chatgpt_hosts() { + let store = ChatGptCloudflareCookieStore::default(); let url = reqwest::Url::parse("https://chatgpt.com/backend-api/codex/responses").unwrap(); - let set_cookie = HeaderValue::from_static("_cfuvid=visitor; Path=/; Secure; HttpOnly"); + let cfuvid = HeaderValue::from_static("_cfuvid=visitor; Path=/; Secure; HttpOnly"); + let clearance = + HeaderValue::from_static("cf_clearance=clearance; Path=/; Secure; HttpOnly"); - store.set_cookies(&mut std::iter::once(&set_cookie), &url); + store.set_cookies(&mut [&cfuvid, &clearance].into_iter(), &url); assert_eq!( store .cookies(&url) .and_then(|value| value.to_str().ok().map(str::to_string)), - Some("_cfuvid=visitor".to_string()) + Some("_cfuvid=visitor; cf_clearance=clearance".to_string()) ); } #[test] fn ignores_non_chatgpt_cookies() { - let store = ChatGptCookieStore::default(); + let store = ChatGptCloudflareCookieStore::default(); let url = reqwest::Url::parse("https://api.openai.com/v1/responses").unwrap(); let set_cookie = HeaderValue::from_static("_cfuvid=visitor; Path=/; Secure; HttpOnly"); @@ -91,8 +152,39 @@ mod tests { } #[test] - fn does_not_return_chatgpt_cookies_for_other_hosts() { - let store = ChatGptCookieStore::default(); + fn ignores_non_cloudflare_cookies_for_chatgpt_hosts() { + let store = ChatGptCloudflareCookieStore::default(); + let url = reqwest::Url::parse("https://chatgpt.com/backend-api/codex/responses").unwrap(); + let set_cookie = HeaderValue::from_static( + "__Secure-next-auth.session-token=secret; Path=/; Secure; HttpOnly", + ); + + store.set_cookies(&mut std::iter::once(&set_cookie), &url); + + assert_eq!(store.cookies(&url), None); + } + + #[test] + fn ignores_mixed_non_cloudflare_cookies_for_chatgpt_hosts() { + let store = ChatGptCloudflareCookieStore::default(); + let url = reqwest::Url::parse("https://chatgpt.com/backend-api/codex/responses").unwrap(); + let cfuvid = HeaderValue::from_static("_cfuvid=visitor; Path=/; Secure; HttpOnly"); + let account_cookie = + HeaderValue::from_static("chatgpt_session=secret; Path=/; Secure; HttpOnly"); + + store.set_cookies(&mut [&cfuvid, &account_cookie].into_iter(), &url); + + assert_eq!( + store + .cookies(&url) + .and_then(|value| value.to_str().ok().map(str::to_string)), + Some("_cfuvid=visitor".to_string()) + ); + } + + #[test] + fn does_not_return_chatgpt_cloudflare_cookies_for_other_hosts() { + let store = ChatGptCloudflareCookieStore::default(); let chatgpt_url = reqwest::Url::parse("https://chatgpt.com/backend-api/codex/responses").unwrap(); let api_url = reqwest::Url::parse("https://api.openai.com/v1/responses").unwrap(); @@ -109,4 +201,31 @@ mod tests { assert!(!is_chatgpt_cookie_url(&url)); } + + #[test] + fn allows_only_known_cloudflare_cookie_names() { + for name in [ + "__cf_bm", + "__cflb", + "__cfruid", + "__cfseq", + "__cfwaitingroom", + "_cfuvid", + "cf_clearance", + "cf_ob_info", + "cf_use_ob", + "cf_chl_rc_i", + ] { + assert!(is_allowed_cloudflare_cookie_name(name)); + } + + for name in [ + "__Secure-next-auth.session-token", + "chatgpt_session", + "oai-auth-token", + "not_cf_clearance", + ] { + assert!(!is_allowed_cloudflare_cookie_name(name)); + } + } } diff --git a/codex-rs/codex-client/src/lib.rs b/codex-rs/codex-client/src/lib.rs index 310ee78eec2f..f3f95f813947 100644 --- a/codex-rs/codex-client/src/lib.rs +++ b/codex-rs/codex-client/src/lib.rs @@ -9,7 +9,7 @@ mod sse; mod telemetry; mod transport; -pub use crate::chatgpt_cookies::with_chatgpt_cookie_store; +pub use crate::chatgpt_cookies::with_chatgpt_cloudflare_cookie_store; pub use crate::chatgpt_hosts::is_allowed_chatgpt_host; pub use crate::custom_ca::BuildCustomCaTransportError; /// Test-only subprocess hook for custom CA coverage. diff --git a/codex-rs/login/src/auth/default_client.rs b/codex-rs/login/src/auth/default_client.rs index e05d86051bad..45c80a36d5b1 100644 --- a/codex-rs/login/src/auth/default_client.rs +++ b/codex-rs/login/src/auth/default_client.rs @@ -8,7 +8,7 @@ use codex_client::BuildCustomCaTransportError; use codex_client::CodexHttpClient; pub use codex_client::CodexRequestBuilder; use codex_client::build_reqwest_client_with_custom_ca; -use codex_client::with_chatgpt_cookie_store; +use codex_client::with_chatgpt_cloudflare_cookie_store; use codex_terminal_detection::user_agent; use reqwest::header::HeaderMap; use reqwest::header::HeaderValue; @@ -202,12 +202,12 @@ pub fn create_client() -> CodexHttpClient { pub fn build_reqwest_client() -> reqwest::Client { try_build_reqwest_client().unwrap_or_else(|error| { tracing::warn!(error = %error, "failed to build default reqwest client"); - with_chatgpt_cookie_store(reqwest::Client::builder()) + with_chatgpt_cloudflare_cookie_store(reqwest::Client::builder()) .build() .unwrap_or_else(|fallback_error| { tracing::warn!( error = %fallback_error, - "failed to build fallback reqwest client with ChatGPT cookie store" + "failed to build fallback reqwest client with ChatGPT Cloudflare cookie store" ); reqwest::Client::new() }) @@ -228,7 +228,7 @@ pub fn try_build_reqwest_client() -> Result Date: Tue, 21 Apr 2026 10:46:14 -0700 Subject: [PATCH 4/7] Address review comment: clarify ChatGPT host matching --- codex-rs/codex-client/src/chatgpt_hosts.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/codex-rs/codex-client/src/chatgpt_hosts.rs b/codex-rs/codex-client/src/chatgpt_hosts.rs index 1303618cf1a3..dd0b99589cad 100644 --- a/codex-rs/codex-client/src/chatgpt_hosts.rs +++ b/codex-rs/codex-client/src/chatgpt_hosts.rs @@ -1,11 +1,13 @@ /// Returns whether `host` is one of the ChatGPT hosts Codex is allowed to treat /// as first-party ChatGPT traffic. pub fn is_allowed_chatgpt_host(host: &str) -> bool { - host == "chatgpt.com" - || host.ends_with(".chatgpt.com") - || host == "chat.openai.com" - || host == "chatgpt-staging.com" - || host.ends_with(".chatgpt-staging.com") + const EXACT_HOSTS: &[&str] = &["chatgpt.com", "chat.openai.com", "chatgpt-staging.com"]; + const SUBDOMAIN_SUFFIXES: &[&str] = &[".chatgpt.com", ".chatgpt-staging.com"]; + + EXACT_HOSTS.contains(&host) + || SUBDOMAIN_SUFFIXES + .iter() + .any(|suffix| host.ends_with(suffix)) } #[cfg(test)] @@ -17,6 +19,7 @@ mod tests { for host in [ "chatgpt.com", "foo.chatgpt.com", + "staging.chatgpt.com", "chat.openai.com", "chatgpt-staging.com", "api.chatgpt-staging.com", From 5fb51315442fb9c46e67ab9759b27d4a5cbe1d04 Mon Sep 17 00:00:00 2001 From: shijie-openai Date: Tue, 21 Apr 2026 10:57:08 -0700 Subject: [PATCH 5/7] Address review comment: separate host policies --- codex-rs/Cargo.lock | 1 - codex-rs/app-server/Cargo.toml | 1 - .../src/transport/remote_control/protocol.rs | 21 +++------ codex-rs/codex-client/src/chatgpt_cookies.rs | 43 ++++++++++++++++--- 4 files changed, 44 insertions(+), 22 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 8f59f809df27..158d7fff5e47 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1468,7 +1468,6 @@ dependencies = [ "codex-arg0", "codex-backend-client", "codex-chatgpt", - "codex-client", "codex-cloud-requirements", "codex-config", "codex-core", diff --git a/codex-rs/app-server/Cargo.toml b/codex-rs/app-server/Cargo.toml index 4c119f4136e6..ea49058e6cf5 100644 --- a/codex-rs/app-server/Cargo.toml +++ b/codex-rs/app-server/Cargo.toml @@ -45,7 +45,6 @@ codex-utils-pty = { workspace = true } codex-backend-client = { workspace = true } codex-file-search = { workspace = true } codex-chatgpt = { workspace = true } -codex-client = { workspace = true } codex-login = { workspace = true } codex-mcp = { workspace = true } codex-models-manager = { workspace = true } diff --git a/codex-rs/app-server/src/transport/remote_control/protocol.rs b/codex-rs/app-server/src/transport/remote_control/protocol.rs index e639d34a4c64..f0db5ecacb57 100644 --- a/codex-rs/app-server/src/transport/remote_control/protocol.rs +++ b/codex-rs/app-server/src/transport/remote_control/protocol.rs @@ -1,6 +1,5 @@ use crate::outgoing_message::OutgoingMessage; use codex_app_server_protocol::JSONRPCMessage; -use codex_client::is_allowed_chatgpt_host; use serde::Deserialize; use serde::Serialize; use std::io; @@ -110,7 +109,10 @@ fn is_allowed_remote_control_chatgpt_host(host: &Option>) -> bool { let Some(Host::Domain(host)) = *host else { return false; }; - is_allowed_chatgpt_host(host) + host == "chatgpt.com" + || host == "chatgpt-staging.com" + || host.ends_with(".chatgpt.com") + || host.ends_with(".chatgpt-staging.com") } fn is_localhost(host: &Option>) -> bool { @@ -135,7 +137,7 @@ pub(super) fn normalize_remote_control_url( io::Error::new( ErrorKind::InvalidInput, format!( - "invalid remote control URL `{remote_control_url}`; expected HTTPS URL for a ChatGPT host, or HTTP/HTTPS URL for localhost" + "invalid remote control URL `{remote_control_url}`; expected HTTPS URL for chatgpt.com or chatgpt-staging.com, or HTTP/HTTPS URL for localhost" ), ) }; @@ -198,16 +200,6 @@ mod tests { .to_string(), } ); - assert_eq!( - normalize_remote_control_url("https://chat.openai.com/backend-api") - .expect("chat.openai.com URL should normalize"), - RemoteControlTarget { - websocket_url: "wss://chat.openai.com/backend-api/wham/remote/control/server" - .to_string(), - enroll_url: "https://chat.openai.com/backend-api/wham/remote/control/server/enroll" - .to_string(), - } - ); } #[test] @@ -240,6 +232,7 @@ mod tests { "http://chatgpt.com/backend-api", "http://example.com/backend-api", "https://example.com/backend-api", + "https://chat.openai.com/backend-api", "https://chatgpt.com.evil.com/backend-api", "https://evilchatgpt.com/backend-api", "https://foo.localhost/backend-api", @@ -251,7 +244,7 @@ mod tests { assert_eq!( err.to_string(), format!( - "invalid remote control URL `{remote_control_url}`; expected HTTPS URL for a ChatGPT host, or HTTP/HTTPS URL for localhost" + "invalid remote control URL `{remote_control_url}`; expected HTTPS URL for chatgpt.com or chatgpt-staging.com, or HTTP/HTTPS URL for localhost" ) ); } diff --git a/codex-rs/codex-client/src/chatgpt_cookies.rs b/codex-rs/codex-client/src/chatgpt_cookies.rs index f40d48c0e315..bc61e2e25b37 100644 --- a/codex-rs/codex-client/src/chatgpt_cookies.rs +++ b/codex-rs/codex-client/src/chatgpt_cookies.rs @@ -57,7 +57,7 @@ pub fn with_chatgpt_cloudflare_cookie_store( fn is_chatgpt_cookie_url(url: &reqwest::Url) -> bool { match url.scheme() { - "http" | "https" => {} + "https" => {} _ => return false, } @@ -132,11 +132,23 @@ mod tests { store.set_cookies(&mut [&cfuvid, &clearance].into_iter(), &url); + let mut cookies = store + .cookies(&url) + .and_then(|value| value.to_str().ok().map(str::to_string)) + .map(|header| { + header + .split("; ") + .map(str::to_string) + .collect::>() + }) + .unwrap_or_default(); + cookies.sort(); assert_eq!( - store - .cookies(&url) - .and_then(|value| value.to_str().ok().map(str::to_string)), - Some("_cfuvid=visitor; cf_clearance=clearance".to_string()) + cookies, + vec![ + "_cfuvid=visitor".to_string(), + "cf_clearance=clearance".to_string() + ] ); } @@ -196,7 +208,26 @@ mod tests { } #[test] - fn only_allows_http_and_https_urls() { + fn rejects_plain_http_chatgpt_cookie_urls() { + let store = ChatGptCloudflareCookieStore::default(); + let http_url = reqwest::Url::parse("http://chatgpt.com/backend-api/codex/responses") + .expect("URL should parse"); + let https_url = reqwest::Url::parse("https://chatgpt.com/backend-api/codex/responses") + .expect("URL should parse"); + let set_cookie = HeaderValue::from_static("_cfuvid=visitor; Path=/; Secure; HttpOnly"); + + store.set_cookies(&mut std::iter::once(&set_cookie), &http_url); + + assert_eq!(store.cookies(&http_url), None); + assert_eq!(store.cookies(&https_url), None); + } + + #[test] + fn only_allows_https_urls() { + let url = reqwest::Url::parse("http://chatgpt.com/backend-api/codex/responses").unwrap(); + + assert!(!is_chatgpt_cookie_url(&url)); + let url = reqwest::Url::parse("wss://chatgpt.com/backend-api/codex/responses").unwrap(); assert!(!is_chatgpt_cookie_url(&url)); From 74ac1f68b891ff5ced7b014f020fba0b61569c17 Mon Sep 17 00:00:00 2001 From: shijie-openai Date: Tue, 21 Apr 2026 11:20:55 -0700 Subject: [PATCH 6/7] Address review comment: rename Cloudflare cookie module --- .../src/{chatgpt_cookies.rs => chatgpt_cloudflare_cookies.rs} | 0 codex-rs/codex-client/src/lib.rs | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename codex-rs/codex-client/src/{chatgpt_cookies.rs => chatgpt_cloudflare_cookies.rs} (100%) diff --git a/codex-rs/codex-client/src/chatgpt_cookies.rs b/codex-rs/codex-client/src/chatgpt_cloudflare_cookies.rs similarity index 100% rename from codex-rs/codex-client/src/chatgpt_cookies.rs rename to codex-rs/codex-client/src/chatgpt_cloudflare_cookies.rs diff --git a/codex-rs/codex-client/src/lib.rs b/codex-rs/codex-client/src/lib.rs index f3f95f813947..acabec17a591 100644 --- a/codex-rs/codex-client/src/lib.rs +++ b/codex-rs/codex-client/src/lib.rs @@ -1,4 +1,4 @@ -mod chatgpt_cookies; +mod chatgpt_cloudflare_cookies; mod chatgpt_hosts; mod custom_ca; mod default_client; @@ -9,7 +9,7 @@ mod sse; mod telemetry; mod transport; -pub use crate::chatgpt_cookies::with_chatgpt_cloudflare_cookie_store; +pub use crate::chatgpt_cloudflare_cookies::with_chatgpt_cloudflare_cookie_store; pub use crate::chatgpt_hosts::is_allowed_chatgpt_host; pub use crate::custom_ca::BuildCustomCaTransportError; /// Test-only subprocess hook for custom CA coverage. From 6189c17b7bcf87740ec75a4491231f1776214800 Mon Sep 17 00:00:00 2001 From: shijie-openai Date: Tue, 21 Apr 2026 12:15:03 -0700 Subject: [PATCH 7/7] Address review comment: cite Cloudflare cookie docs --- codex-rs/codex-client/src/chatgpt_cloudflare_cookies.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/codex-rs/codex-client/src/chatgpt_cloudflare_cookies.rs b/codex-rs/codex-client/src/chatgpt_cloudflare_cookies.rs index bc61e2e25b37..c5f4bbd4eb1e 100644 --- a/codex-rs/codex-client/src/chatgpt_cloudflare_cookies.rs +++ b/codex-rs/codex-client/src/chatgpt_cloudflare_cookies.rs @@ -102,6 +102,8 @@ fn only_cloudflare_cookies(header: HeaderValue) -> Option { } fn is_allowed_cloudflare_cookie_name(name: &str) -> bool { + // Keep this allowlist aligned with Cloudflare's documented service cookies: + // https://developers.cloudflare.com/fundamentals/reference/policies-compliances/cloudflare-cookies/ matches!( name, "__cf_bm"