Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion codex-rs/codex-api/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ pub struct ResponsesApiRequest {
pub prompt_cache_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<TextControls>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_metadata: Option<HashMap<String, String>>,
}

impl From<&ResponsesApiRequest> for ResponseCreateWsRequest {
Expand All @@ -189,7 +191,7 @@ impl From<&ResponsesApiRequest> for ResponseCreateWsRequest {
prompt_cache_key: request.prompt_cache_key.clone(),
text: request.text.clone(),
generate: None,
client_metadata: None,
client_metadata: request.client_metadata.clone(),
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/codex-api/tests/clients.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ async fn streaming_client_retries_on_transport_error() -> Result<()> {
service_tier: None,
prompt_cache_key: None,
text: None,
client_metadata: None,
};
let client = ResponsesClient::new(transport.clone(), provider, NoAuth);

Expand Down Expand Up @@ -320,6 +321,7 @@ async fn azure_default_store_attaches_ids_and_headers() -> Result<()> {
service_tier: None,
prompt_cache_key: None,
text: None,
client_metadata: None,
};

let mut extra_headers = HeaderMap::new();
Expand Down
18 changes: 17 additions & 1 deletion codex-rs/core/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ use codex_response_debug_context::telemetry_api_error_message;
use codex_response_debug_context::telemetry_transport_error_message;

pub const OPENAI_BETA_HEADER: &str = "OpenAI-Beta";
pub const X_CODEX_INSTALLATION_ID_HEADER: &str = "x-codex-installation-id";
pub const X_CODEX_TURN_STATE_HEADER: &str = "x-codex-turn-state";
pub const X_CODEX_TURN_METADATA_HEADER: &str = "x-codex-turn-metadata";
pub const X_CODEX_PARENT_THREAD_ID_HEADER: &str = "x-codex-parent-thread-id";
Expand All @@ -142,6 +143,7 @@ struct ModelClientState {
auth_manager: Option<Arc<AuthManager>>,
conversation_id: ThreadId,
window_generation: AtomicU64,
installation_id: String,
provider: ModelProviderInfo,
auth_env_telemetry: AuthEnvTelemetry,
session_source: SessionSource,
Expand Down Expand Up @@ -263,6 +265,7 @@ impl ModelClient {
pub fn new(
auth_manager: Option<Arc<AuthManager>>,
conversation_id: ThreadId,
installation_id: String,
provider: ModelProviderInfo,
session_source: SessionSource,
model_verbosity: Option<VerbosityConfig>,
Expand All @@ -280,6 +283,7 @@ impl ModelClient {
auth_manager,
conversation_id,
window_generation: AtomicU64::new(0),
installation_id,
provider,
auth_env_telemetry,
session_source,
Expand Down Expand Up @@ -429,7 +433,11 @@ impl ModelClient {
text,
};

let mut extra_headers = self.build_responses_identity_headers();
let mut extra_headers = ApiHeaderMap::new();
if let Ok(header_value) = HeaderValue::from_str(&self.state.installation_id) {
extra_headers.insert(X_CODEX_INSTALLATION_ID_HEADER, header_value);
}
extra_headers.extend(self.build_responses_identity_headers());
extra_headers.extend(build_conversation_headers(Some(
self.state.conversation_id.to_string(),
)));
Expand Down Expand Up @@ -515,6 +523,10 @@ impl ModelClient {
turn_metadata_header: Option<&str>,
) -> HashMap<String, String> {
let mut client_metadata = HashMap::new();
client_metadata.insert(
X_CODEX_INSTALLATION_ID_HEADER.to_string(),
self.state.installation_id.clone(),
);
client_metadata.insert(
X_CODEX_WINDOW_ID_HEADER.to_string(),
self.current_window_id(),
Expand Down Expand Up @@ -817,6 +829,10 @@ impl ModelClientSession {
},
prompt_cache_key,
text,
client_metadata: Some(HashMap::from([(
X_CODEX_INSTALLATION_ID_HEADER.to_string(),
self.client.state.installation_id.clone(),
)])),
};
Ok(request)
}
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/core/src/client_common_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ fn serializes_text_verbosity_when_set() {
verbosity: Some(OpenAiVerbosity::Low),
format: None,
}),
client_metadata: None,
};

let v = serde_json::to_value(&req).expect("json");
Expand Down Expand Up @@ -69,6 +70,7 @@ fn serializes_text_schema_with_strict_format() {
prompt_cache_key: None,
service_tier: None,
text: Some(text_controls),
client_metadata: None,
};

let v = serde_json::to_value(&req).expect("json");
Expand Down Expand Up @@ -106,6 +108,7 @@ fn omits_text_when_not_set() {
prompt_cache_key: None,
service_tier: None,
text: None,
client_metadata: None,
};

let v = serde_json::to_value(&req).expect("json");
Expand All @@ -128,6 +131,7 @@ fn serializes_flex_service_tier_when_set() {
prompt_cache_key: None,
service_tier: Some(ServiceTier::Flex.to_string()),
text: None,
client_metadata: None,
};

let v = serde_json::to_value(&req).expect("json");
Expand Down
6 changes: 6 additions & 0 deletions codex-rs/core/src/client_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use super::AuthRequestTelemetryContext;
use super::ModelClient;
use super::PendingUnauthorizedRetry;
use super::UnauthorizedRecoveryExecution;
use super::X_CODEX_INSTALLATION_ID_HEADER;
use super::X_CODEX_PARENT_THREAD_ID_HEADER;
use super::X_CODEX_TURN_METADATA_HEADER;
use super::X_CODEX_WINDOW_ID_HEADER;
Expand All @@ -23,6 +24,7 @@ fn test_model_client(session_source: SessionSource) -> ModelClient {
ModelClient::new(
/*auth_manager*/ None,
ThreadId::new(),
/*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(),
provider,
session_source,
/*model_verbosity*/ None,
Expand Down Expand Up @@ -107,6 +109,10 @@ fn build_ws_client_metadata_includes_window_lineage_and_turn_metadata() {
assert_eq!(
client_metadata,
std::collections::HashMap::from([
(
X_CODEX_INSTALLATION_ID_HEADER.to_string(),
"11111111-1111-4111-8111-111111111111".to_string(),
),
(
X_CODEX_WINDOW_ID_HEADER.to_string(),
format!("{conversation_id}:1"),
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/core/src/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use crate::compact_remote::run_inline_remote_auto_compact_task;
use crate::config::ManagedFeatures;
use crate::connectors;
use crate::exec_policy::ExecPolicyManager;
use crate::installation_id::resolve_installation_id;
use crate::parse_turn_item;
use crate::path_utils::normalize_for_native_workdir;
use crate::realtime_conversation::RealtimeConversationManager;
Expand Down Expand Up @@ -1911,6 +1912,7 @@ impl Session {
});
}

let installation_id = resolve_installation_id(&config.codex_home).await?;
let services = SessionServices {
// Initialize the MCP connection manager with an uninitialized
// instance. It will be replaced with one created via
Expand Down Expand Up @@ -1954,6 +1956,7 @@ impl Session {
model_client: ModelClient::new(
Some(Arc::clone(&auth_manager)),
conversation_id,
installation_id,
session_configuration.provider.clone(),
session_configuration.session_source.clone(),
config.model_verbosity,
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/core/src/codex_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ fn test_model_client_session() -> crate::client::ModelClientSession {
/*auth_manager*/ None,
ThreadId::try_from("00000000-0000-4000-8000-000000000001")
.expect("test thread id should be valid"),
/*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(),
ModelProviderInfo::create_openai_provider(/* base_url */ /*base_url*/ None),
codex_protocol::protocol::SessionSource::Exec,
/*model_verbosity*/ None,
Expand Down Expand Up @@ -2766,6 +2767,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
model_client: ModelClient::new(
Some(auth_manager.clone()),
conversation_id,
/*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(),
session_configuration.provider.clone(),
session_configuration.session_source.clone(),
config.model_verbosity,
Expand Down Expand Up @@ -3606,6 +3608,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx(
model_client: ModelClient::new(
Some(Arc::clone(&auth_manager)),
conversation_id,
/*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(),
session_configuration.provider.clone(),
session_configuration.session_source.clone(),
config.model_verbosity,
Expand Down
145 changes: 145 additions & 0 deletions codex-rs/core/src/installation_id.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
use std::fs::OpenOptions;
use std::io::Read;
use std::io::Result;
use std::io::Seek;
use std::io::SeekFrom;
use std::io::Write;
use std::path::Path;

#[cfg(unix)]
use std::os::unix::fs::OpenOptionsExt;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;

use tokio::fs;
use uuid::Uuid;

pub(crate) const INSTALLATION_ID_FILENAME: &str = "installation_id";

pub(crate) async fn resolve_installation_id(codex_home: &Path) -> Result<String> {
let path = codex_home.join(INSTALLATION_ID_FILENAME);
fs::create_dir_all(codex_home).await?;
tokio::task::spawn_blocking(move || {
let mut options = OpenOptions::new();
options.read(true).write(true).create(true);

#[cfg(unix)]
{
options.mode(0o644);
}

let mut file = options.open(&path)?;
file.lock()?;

#[cfg(unix)]
{
let metadata = file.metadata()?;
let current_mode = metadata.permissions().mode() & 0o777;
if current_mode != 0o644 {
let mut permissions = metadata.permissions();
permissions.set_mode(0o644);
file.set_permissions(permissions)?;
}
}

let mut contents = String::new();
file.read_to_string(&mut contents)?;
let trimmed = contents.trim();
if !trimmed.is_empty()
&& let Ok(existing) = Uuid::parse_str(trimmed)
{
return Ok(existing.to_string());
}

let installation_id = Uuid::new_v4().to_string();
file.set_len(0)?;
file.seek(SeekFrom::Start(0))?;
file.write_all(installation_id.as_bytes())?;
file.flush()?;
file.sync_all()?;

Ok(installation_id)
})
.await?
}

#[cfg(test)]
mod tests {
use super::INSTALLATION_ID_FILENAME;
use super::resolve_installation_id;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
use uuid::Uuid;

#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;

#[tokio::test]
async fn resolve_installation_id_generates_and_persists_uuid() {
let codex_home = TempDir::new().expect("create temp dir");
let persisted_path = codex_home.path().join(INSTALLATION_ID_FILENAME);

let installation_id = resolve_installation_id(codex_home.path())
.await
.expect("resolve installation id");

assert_eq!(
std::fs::read_to_string(&persisted_path).expect("read persisted installation id"),
installation_id
);
assert!(Uuid::parse_str(&installation_id).is_ok());

#[cfg(unix)]
{
let mode = std::fs::metadata(&persisted_path)
.expect("read installation id metadata")
.permissions()
.mode()
& 0o777;
assert_eq!(mode, 0o644);
}
}

#[tokio::test]
async fn resolve_installation_id_reuses_existing_uuid() {
let codex_home = TempDir::new().expect("create temp dir");
let existing = Uuid::new_v4().to_string().to_uppercase();
std::fs::write(
codex_home.path().join(INSTALLATION_ID_FILENAME),
existing.clone(),
)
.expect("write installation id");

let resolved = resolve_installation_id(codex_home.path())
.await
.expect("resolve installation id");

assert_eq!(
resolved,
Uuid::parse_str(existing.as_str())
.expect("parse existing installation id")
.to_string()
);
}

#[tokio::test]
async fn resolve_installation_id_rewrites_invalid_file_contents() {
let codex_home = TempDir::new().expect("create temp dir");
std::fs::write(
codex_home.path().join(INSTALLATION_ID_FILENAME),
"not-a-uuid",
)
.expect("write invalid installation id");

let resolved = resolve_installation_id(codex_home.path())
.await
.expect("resolve installation id");

assert!(Uuid::parse_str(&resolved).is_ok());
assert_eq!(
std::fs::read_to_string(codex_home.path().join(INSTALLATION_ID_FILENAME))
.expect("read rewritten installation id"),
resolved
);
}
}
2 changes: 2 additions & 0 deletions codex-rs/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ mod flags;
mod git_info_tests;
mod guardian;
mod hook_runtime;
mod installation_id;
pub(crate) mod instructions;
pub(crate) mod landlock;
pub use landlock::spawn_command_under_linux_sandbox;
Expand Down Expand Up @@ -179,6 +180,7 @@ pub mod util;

pub use client::ModelClient;
pub use client::ModelClientSession;
pub use client::X_CODEX_INSTALLATION_ID_HEADER;
pub use client::X_CODEX_TURN_METADATA_HEADER;
pub use client_common::Prompt;
pub use client_common::REVIEW_PROMPT;
Expand Down
Loading
Loading