[codex-http-state] extract generic per-surface HTTP state store [ci changed_files]

This commit is contained in:
Cooper Gamble
2026-06-03 02:11:07 +00:00
parent d0b19e41e5
commit e943a052a8
8 changed files with 167 additions and 218 deletions
+11 -1
View File
@@ -2319,7 +2319,6 @@ dependencies = [
"async-trait",
"bytes",
"codex-utils-cargo-bin",
"codex-utils-path",
"codex-utils-rustls-provider",
"eventsource-stream",
"futures",
@@ -3029,6 +3028,17 @@ dependencies = [
"uuid",
]
[[package]]
name = "codex-http-state"
version = "0.0.0"
dependencies = [
"codex-utils-path",
"pretty_assertions",
"serde",
"serde_json",
"tempfile",
]
[[package]]
name = "codex-image-generation-extension"
version = "0.0.0"
+2
View File
@@ -37,6 +37,7 @@ members = [
"core-plugins",
"core-skills",
"hooks",
"http-state",
"secrets",
"exec",
"file-system",
@@ -177,6 +178,7 @@ codex-file-search = { path = "file-search" }
codex-file-watcher = { path = "file-watcher" }
codex-git-utils = { path = "git-utils" }
codex-hooks = { path = "hooks" }
codex-http-state = { path = "http-state" }
codex-keyring-store = { path = "keyring-store" }
codex-linux-sandbox = { path = "linux-sandbox" }
codex-lmstudio = { path = "lmstudio" }
-1
View File
@@ -7,7 +7,6 @@ version.workspace = true
[dependencies]
async-trait = { workspace = true }
bytes = { workspace = true }
codex-utils-path = { workspace = true }
eventsource-stream = { workspace = true }
futures = { workspace = true }
http = { workspace = true }
+2 -7
View File
@@ -3,7 +3,6 @@ mod chatgpt_hosts;
mod custom_ca;
mod default_client;
mod error;
mod native_integrity;
mod request;
mod retry;
mod sse;
@@ -27,12 +26,8 @@ pub use crate::default_client::CodexHttpClient;
pub use crate::default_client::CodexRequestBuilder;
pub use crate::error::StreamError;
pub use crate::error::TransportError;
pub use crate::native_integrity::INTEGRITY_STATE_HEADER_NAME;
pub use crate::native_integrity::INTEGRITY_STATE_UPDATE_HEADER_NAME;
pub use crate::native_integrity::NativeIntegrityStateContext;
pub use crate::native_integrity::NativeIntegrityStateFile;
pub use crate::native_integrity::NativeIntegrityStateStore;
pub use crate::native_integrity::NativeIntegritySurface;
pub const INTEGRITY_STATE_HEADER_NAME: &str = "X-OAI-IS";
pub const INTEGRITY_STATE_UPDATE_HEADER_NAME: &str = "X-OAI-IS-Update";
pub use crate::request::PreparedRequestBody;
pub use crate::request::Request;
pub use crate::request::RequestBody;
@@ -1,120 +0,0 @@
use super::*;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
const STATE_N: &str = "ois1.a.b.c";
const STATE_N_PLUS_ONE: &str = "ois1.d.e.f";
#[test]
fn maps_app_server_client_names_to_bounded_surfaces() {
assert_eq!(
[
"codex-tui",
"codex_exec",
"codex_vscode",
"codex_desktop",
"codex_desktop_ssh",
"codex_remote_control",
"third_party_client",
]
.map(NativeIntegritySurface::from_app_server_client_name),
[
NativeIntegritySurface::CodexTui,
NativeIntegritySurface::CodexExec,
NativeIntegritySurface::CodexVscode,
NativeIntegritySurface::CodexDesktop,
NativeIntegritySurface::CodexDesktopSsh,
NativeIntegritySurface::CodexRemoteControl,
NativeIntegritySurface::CodexCli,
]
);
}
#[test]
fn stores_state_in_one_file_per_surface() {
let codex_home = TempDir::new().expect("tempdir");
let store = NativeIntegrityStateStore::new(codex_home.path().to_path_buf());
store
.replace(NativeIntegritySurface::CodexCli, STATE_N.to_string())
.expect("CLI state should store");
store
.replace(
NativeIntegritySurface::CodexDesktop,
STATE_N_PLUS_ONE.to_string(),
)
.expect("desktop state should store");
assert_eq!(
store
.load(NativeIntegritySurface::CodexCli)
.expect("CLI state should load")
.expect("CLI state should exist"),
NativeIntegrityStateFile {
state: STATE_N.to_string(),
}
);
assert_eq!(
store
.load(NativeIntegritySurface::CodexDesktop)
.expect("desktop state should load")
.expect("desktop state should exist"),
NativeIntegrityStateFile {
state: STATE_N_PLUS_ONE.to_string(),
}
);
assert_eq!(
store.state_path(NativeIntegritySurface::CodexCli),
codex_home.path().join("state/codex_cli.json")
);
store
.clear(NativeIntegritySurface::CodexDesktop)
.expect("desktop state should clear");
assert_eq!(
store
.load(NativeIntegritySurface::CodexDesktop)
.expect("desktop state should load"),
None
);
}
#[test]
fn compare_and_store_rejects_a_stale_prior_value() {
let codex_home = TempDir::new().expect("tempdir");
let store = NativeIntegrityStateStore::new(codex_home.path().to_path_buf());
store
.replace(NativeIntegritySurface::CodexCli, STATE_N.to_string())
.expect("state should store");
assert!(
!store
.compare_and_store(
NativeIntegritySurface::CodexCli,
"ois1.stale.state.value",
STATE_N_PLUS_ONE.to_string(),
)
.expect("compare should succeed")
);
assert!(
store
.compare_and_store(
NativeIntegritySurface::CodexCli,
STATE_N,
STATE_N_PLUS_ONE.to_string(),
)
.expect("compare should succeed")
);
}
#[test]
fn validates_integrity_state_envelopes() {
assert!(is_valid_integrity_state_envelope(STATE_N));
assert!(!is_valid_integrity_state_envelope(""));
assert!(!is_valid_integrity_state_envelope("state"));
assert!(!is_valid_integrity_state_envelope(&format!(" {STATE_N}")));
assert!(!is_valid_integrity_state_envelope("ois1.a.b"));
assert!(!is_valid_integrity_state_envelope(&format!(
"ois1.a.b.{}",
"c".repeat(MAX_INTEGRITY_STATE_ENVELOPE_BYTES + 1)
)));
}
+17
View File
@@ -0,0 +1,17 @@
[package]
edition.workspace = true
license.workspace = true
name = "codex-http-state"
version.workspace = true
[dependencies]
codex-utils-path = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
[dev-dependencies]
pretty_assertions = { workspace = true }
tempfile = { workspace = true }
[lints]
workspace = true
@@ -10,19 +10,15 @@ use codex_utils_path::write_atomically;
use serde::Deserialize;
use serde::Serialize;
pub const INTEGRITY_STATE_HEADER_NAME: &str = "X-OAI-IS";
pub const INTEGRITY_STATE_UPDATE_HEADER_NAME: &str = "X-OAI-IS-Update";
const MAX_INTEGRITY_STATE_ENVELOPE_BYTES: usize = 2048;
const STATE_DIR_NAME: &str = "state";
/// Native Codex surface that owns one local integrity-state file.
/// Native Codex surface that owns one local HTTP-state file.
///
/// Unknown app-server clients intentionally share the CLI state file. This
/// preserves the default classification while first-party clients opt into a
/// more specific surface by setting `clientInfo.name` during initialization.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum NativeIntegritySurface {
pub enum HttpStateSurface {
CodexCli,
CodexTui,
CodexExec,
@@ -32,7 +28,7 @@ pub enum NativeIntegritySurface {
CodexRemoteControl,
}
impl NativeIntegritySurface {
impl HttpStateSurface {
pub fn from_app_server_client_name(client_name: &str) -> Self {
match client_name {
"codex-tui" => Self::CodexTui,
@@ -58,55 +54,50 @@ impl NativeIntegritySurface {
}
}
impl fmt::Display for NativeIntegritySurface {
impl fmt::Display for HttpStateSurface {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct NativeIntegrityStateFile {
pub state: String,
#[derive(Deserialize, Serialize)]
struct HttpStateFile {
state: String,
}
#[derive(Clone, Debug)]
pub struct NativeIntegrityStateStore {
pub struct HttpStateStore {
codex_home: PathBuf,
}
impl NativeIntegrityStateStore {
impl HttpStateStore {
pub fn new(codex_home: PathBuf) -> Self {
Self { codex_home }
}
fn state_path(&self, surface: NativeIntegritySurface) -> PathBuf {
fn state_path(&self, surface: HttpStateSurface) -> PathBuf {
self.codex_home
.join(STATE_DIR_NAME)
.join(format!("{surface}.json"))
}
pub fn load(
&self,
surface: NativeIntegritySurface,
) -> io::Result<Option<NativeIntegrityStateFile>> {
pub fn get(&self, surface: HttpStateSurface) -> io::Result<Option<String>> {
let path = self.state_path(surface);
let contents = match fs::read(&path) {
Ok(contents) => contents,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(error),
};
let state_file: NativeIntegrityStateFile =
let state_file: HttpStateFile =
serde_json::from_slice(&contents).map_err(io::Error::other)?;
validate_integrity_state_envelope(&state_file.state)?;
Ok(Some(state_file))
Ok(Some(state_file.state))
}
pub fn replace(&self, surface: NativeIntegritySurface, state: String) -> io::Result<()> {
validate_integrity_state_envelope(&state)?;
self.write(surface, &NativeIntegrityStateFile { state })
pub fn set(&self, surface: HttpStateSurface, state: String) -> io::Result<()> {
self.write(surface, &HttpStateFile { state })
}
pub fn clear(&self, surface: NativeIntegritySurface) -> io::Result<()> {
pub fn clear(&self, surface: HttpStateSurface) -> io::Result<()> {
match fs::remove_file(self.state_path(surface)) {
Ok(()) => Ok(()),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
@@ -119,29 +110,24 @@ impl NativeIntegrityStateStore {
/// This is intentionally lock-free. Concurrent writers may both observe
/// the same prior value, in which case atomic replacement keeps the file
/// well-formed and the last writer wins.
pub fn compare_and_store(
pub fn compare_and_set(
&self,
surface: NativeIntegritySurface,
surface: HttpStateSurface,
expected_state: &str,
next_state: String,
) -> io::Result<bool> {
validate_integrity_state_envelope(&next_state)?;
let Some(current) = self.load(surface)? else {
let Some(current) = self.get(surface)? else {
return Ok(false);
};
if current.state != expected_state {
if current != expected_state {
return Ok(false);
}
self.write(surface, &NativeIntegrityStateFile { state: next_state })?;
self.write(surface, &HttpStateFile { state: next_state })?;
Ok(true)
}
fn write(
&self,
surface: NativeIntegritySurface,
state_file: &NativeIntegrityStateFile,
) -> io::Result<()> {
fn write(&self, surface: HttpStateSurface, state_file: &HttpStateFile) -> io::Result<()> {
let path = self.state_path(surface);
let contents = serde_json::to_string_pretty(state_file).map_err(io::Error::other)?;
write_atomically(&path, &contents)
@@ -155,24 +141,24 @@ impl NativeIntegrityStateStore {
/// auth decorator, so clones of the model client stay in sync without moving
/// in-flight rotations to another surface.
#[derive(Clone, Debug)]
pub struct NativeIntegrityStateContext {
store: NativeIntegrityStateStore,
surface: Arc<RwLock<NativeIntegritySurface>>,
pub struct HttpStateContext {
store: HttpStateStore,
surface: Arc<RwLock<HttpStateSurface>>,
}
impl NativeIntegrityStateContext {
pub fn new(codex_home: PathBuf, surface: NativeIntegritySurface) -> Self {
impl HttpStateContext {
pub fn new(codex_home: PathBuf, surface: HttpStateSurface) -> Self {
Self {
store: NativeIntegrityStateStore::new(codex_home),
store: HttpStateStore::new(codex_home),
surface: Arc::new(RwLock::new(surface)),
}
}
pub fn surface(&self) -> NativeIntegritySurface {
pub fn surface(&self) -> HttpStateSurface {
*self.surface.read().unwrap_or_else(PoisonError::into_inner)
}
pub fn set_surface(&self, surface: NativeIntegritySurface) -> bool {
pub fn set_surface(&self, surface: HttpStateSurface) -> bool {
let mut selected_surface = self.surface.write().unwrap_or_else(PoisonError::into_inner);
if *selected_surface == surface {
return false;
@@ -181,60 +167,20 @@ impl NativeIntegrityStateContext {
true
}
pub fn load_for_surface(
&self,
surface: NativeIntegritySurface,
) -> io::Result<Option<NativeIntegrityStateFile>> {
self.store.load(surface)
pub fn get_for_surface(&self, surface: HttpStateSurface) -> io::Result<Option<String>> {
self.store.get(surface)
}
pub fn compare_and_store_for_surface(
pub fn compare_and_set_for_surface(
&self,
surface: NativeIntegritySurface,
surface: HttpStateSurface,
expected_state: &str,
next_state: String,
) -> io::Result<bool> {
self.store
.compare_and_store(surface, expected_state, next_state)
}
}
fn is_valid_integrity_state_envelope(value: &str) -> bool {
if value.is_empty() || value.len() > MAX_INTEGRITY_STATE_ENVELOPE_BYTES || value.trim() != value
{
return false;
}
let mut parts = value.split('.');
if parts.next() != Some("ois1") {
return false;
}
let valid_part = |part: &str| {
!part.is_empty()
&& part
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_')
};
matches!(
(parts.next(), parts.next(), parts.next(), parts.next()),
(Some(header), Some(nonce), Some(ciphertext), None)
if valid_part(header) && valid_part(nonce) && valid_part(ciphertext)
)
}
fn validate_integrity_state_envelope(value: &str) -> io::Result<()> {
if is_valid_integrity_state_envelope(value) {
Ok(())
} else {
Err(io::Error::new(
io::ErrorKind::InvalidData,
"integrity state envelope is malformed",
))
.compare_and_set(surface, expected_state, next_state)
}
}
#[cfg(test)]
#[path = "native_integrity_tests.rs"]
mod tests;
+100
View File
@@ -0,0 +1,100 @@
use super::*;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
const STATE_N: &str = "ois1.a.b.c";
const STATE_N_PLUS_ONE: &str = "ois1.d.e.f";
#[test]
fn maps_app_server_client_names_to_bounded_surfaces() {
assert_eq!(
[
"codex-tui",
"codex_exec",
"codex_vscode",
"codex_desktop",
"codex_desktop_ssh",
"codex_remote_control",
"third_party_client",
]
.map(HttpStateSurface::from_app_server_client_name),
[
HttpStateSurface::CodexTui,
HttpStateSurface::CodexExec,
HttpStateSurface::CodexVscode,
HttpStateSurface::CodexDesktop,
HttpStateSurface::CodexDesktopSsh,
HttpStateSurface::CodexRemoteControl,
HttpStateSurface::CodexCli,
]
);
}
#[test]
fn stores_state_in_one_file_per_surface() {
let codex_home = TempDir::new().expect("tempdir");
let store = HttpStateStore::new(codex_home.path().to_path_buf());
store
.set(HttpStateSurface::CodexCli, STATE_N.to_string())
.expect("CLI state should store");
store
.set(HttpStateSurface::CodexDesktop, STATE_N_PLUS_ONE.to_string())
.expect("desktop state should store");
assert_eq!(
store
.get(HttpStateSurface::CodexCli)
.expect("CLI state should load")
.expect("CLI state should exist"),
STATE_N
);
assert_eq!(
store
.get(HttpStateSurface::CodexDesktop)
.expect("desktop state should load")
.expect("desktop state should exist"),
STATE_N_PLUS_ONE
);
assert_eq!(
store.state_path(HttpStateSurface::CodexCli),
codex_home.path().join("state/codex_cli.json")
);
store
.clear(HttpStateSurface::CodexDesktop)
.expect("desktop state should clear");
assert_eq!(
store
.get(HttpStateSurface::CodexDesktop)
.expect("desktop state should load"),
None
);
}
#[test]
fn compare_and_set_rejects_a_stale_prior_value() {
let codex_home = TempDir::new().expect("tempdir");
let store = HttpStateStore::new(codex_home.path().to_path_buf());
store
.set(HttpStateSurface::CodexCli, STATE_N.to_string())
.expect("state should store");
assert!(
!store
.compare_and_set(
HttpStateSurface::CodexCli,
"ois1.stale.state.value",
STATE_N_PLUS_ONE.to_string(),
)
.expect("compare should succeed")
);
assert!(
store
.compare_and_set(
HttpStateSurface::CodexCli,
STATE_N,
STATE_N_PLUS_ONE.to_string(),
)
.expect("compare should succeed")
);
}