mirror of
https://github.com/openai/codex.git
synced 2026-04-04 14:31:40 +03:00
Compare commits
1 Commits
mstar/remo
...
owen/devic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c50c42959 |
@@ -113,6 +113,7 @@ use crate::util::emit_feedback_auth_recovery_tags;
|
||||
use crate::util::emit_feedback_request_tags_with_auth_env;
|
||||
|
||||
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_RESPONSESAPI_INCLUDE_TIMING_METRICS_HEADER: &str =
|
||||
@@ -133,6 +134,7 @@ pub(crate) const WEBSOCKET_CONNECT_TIMEOUT: Duration =
|
||||
struct ModelClientState {
|
||||
auth_manager: Option<Arc<AuthManager>>,
|
||||
conversation_id: ThreadId,
|
||||
installation_id: String,
|
||||
provider: ModelProviderInfo,
|
||||
auth_env_telemetry: AuthEnvTelemetry,
|
||||
session_source: SessionSource,
|
||||
@@ -254,6 +256,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>,
|
||||
@@ -269,6 +272,7 @@ impl ModelClient {
|
||||
state: Arc::new(ModelClientState {
|
||||
auth_manager,
|
||||
conversation_id,
|
||||
installation_id,
|
||||
provider,
|
||||
auth_env_telemetry,
|
||||
session_source,
|
||||
@@ -393,6 +397,9 @@ impl ModelClient {
|
||||
};
|
||||
|
||||
let mut extra_headers = self.build_subagent_headers();
|
||||
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(build_conversation_headers(Some(
|
||||
self.state.conversation_id.to_string(),
|
||||
)));
|
||||
@@ -640,6 +647,7 @@ impl ModelClient {
|
||||
let turn_metadata_header = parse_turn_metadata_header(turn_metadata_header);
|
||||
let conversation_id = self.state.conversation_id.to_string();
|
||||
let mut headers = build_responses_headers(
|
||||
Some(self.state.installation_id.as_str()),
|
||||
self.state.beta_features_header.as_deref(),
|
||||
turn_state,
|
||||
turn_metadata_header.as_ref(),
|
||||
@@ -763,6 +771,7 @@ impl ModelClientSession {
|
||||
conversation_id: Some(conversation_id),
|
||||
session_source: Some(self.client.state.session_source.clone()),
|
||||
extra_headers: build_responses_headers(
|
||||
Some(self.client.state.installation_id.as_str()),
|
||||
self.client.state.beta_features_header.as_deref(),
|
||||
Some(&self.turn_state),
|
||||
turn_metadata_header.as_ref(),
|
||||
@@ -1375,15 +1384,22 @@ fn build_ws_client_metadata(turn_metadata_header: Option<&str>) -> Option<HashMa
|
||||
///
|
||||
/// These headers implement Codex-specific conventions:
|
||||
///
|
||||
/// - `x-codex-installation-id`: stable identifier persisted under `CODEX_HOME`.
|
||||
/// - `x-codex-beta-features`: comma-separated beta feature keys enabled for the session.
|
||||
/// - `x-codex-turn-state`: sticky routing token captured earlier in the turn.
|
||||
/// - `x-codex-turn-metadata`: optional per-turn metadata for observability.
|
||||
fn build_responses_headers(
|
||||
installation_id: Option<&str>,
|
||||
beta_features_header: Option<&str>,
|
||||
turn_state: Option<&Arc<OnceLock<String>>>,
|
||||
turn_metadata_header: Option<&HeaderValue>,
|
||||
) -> ApiHeaderMap {
|
||||
let mut headers = ApiHeaderMap::new();
|
||||
if let Some(value) = installation_id
|
||||
&& let Ok(header_value) = HeaderValue::from_str(value)
|
||||
{
|
||||
headers.insert(X_CODEX_INSTALLATION_ID_HEADER, header_value);
|
||||
}
|
||||
if let Some(value) = beta_features_header
|
||||
&& !value.is_empty()
|
||||
&& let Ok(header_value) = HeaderValue::from_str(value)
|
||||
|
||||
@@ -18,6 +18,7 @@ fn test_model_client(session_source: SessionSource) -> ModelClient {
|
||||
ModelClient::new(
|
||||
None,
|
||||
ThreadId::new(),
|
||||
"11111111-1111-4111-8111-111111111111".to_string(),
|
||||
provider,
|
||||
session_source,
|
||||
None,
|
||||
|
||||
@@ -23,6 +23,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;
|
||||
#[cfg(test)]
|
||||
use crate::models_manager::collaboration_mode_presets::CollaborationModesConfig;
|
||||
use crate::models_manager::manager::ModelsManager;
|
||||
@@ -1845,6 +1846,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
|
||||
@@ -1888,6 +1890,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,
|
||||
|
||||
@@ -250,6 +250,7 @@ fn test_model_client_session() -> crate::client::ModelClientSession {
|
||||
None,
|
||||
ThreadId::try_from("00000000-0000-4000-8000-000000000001")
|
||||
.expect("test thread id should be valid"),
|
||||
"11111111-1111-4111-8111-111111111111".to_string(),
|
||||
crate::model_provider_info::ModelProviderInfo::create_openai_provider(
|
||||
/* base_url */ None,
|
||||
),
|
||||
@@ -2676,6 +2677,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
model_client: ModelClient::new(
|
||||
Some(auth_manager.clone()),
|
||||
conversation_id,
|
||||
"11111111-1111-4111-8111-111111111111".to_string(),
|
||||
session_configuration.provider.clone(),
|
||||
session_configuration.session_source.clone(),
|
||||
config.model_verbosity,
|
||||
@@ -3513,6 +3515,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,
|
||||
"11111111-1111-4111-8111-111111111111".to_string(),
|
||||
session_configuration.provider.clone(),
|
||||
session_configuration.session_source.clone(),
|
||||
config.model_verbosity,
|
||||
|
||||
198
codex-rs/core/src/installation_id.rs
Normal file
198
codex-rs/core/src/installation_id.rs
Normal file
@@ -0,0 +1,198 @@
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Result;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[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";
|
||||
|
||||
enum InstallationIdWriteMode {
|
||||
CreateNew,
|
||||
Overwrite,
|
||||
}
|
||||
|
||||
pub(crate) async fn resolve_installation_id(codex_home: &Path) -> Result<String> {
|
||||
let path = codex_home.join(INSTALLATION_ID_FILENAME);
|
||||
if let Some(existing) = read_installation_id(&path).await? {
|
||||
return Ok(existing);
|
||||
}
|
||||
|
||||
fs::create_dir_all(codex_home).await?;
|
||||
|
||||
loop {
|
||||
let installation_id = Uuid::new_v4().to_string();
|
||||
match write_installation_id(
|
||||
path.clone(),
|
||||
installation_id.clone(),
|
||||
InstallationIdWriteMode::CreateNew,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => return Ok(installation_id),
|
||||
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
|
||||
if let Some(existing) = read_installation_id(&path).await? {
|
||||
return Ok(existing);
|
||||
}
|
||||
|
||||
write_installation_id(
|
||||
path.clone(),
|
||||
installation_id.clone(),
|
||||
InstallationIdWriteMode::Overwrite,
|
||||
)
|
||||
.await?;
|
||||
return Ok(installation_id);
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
fs::create_dir_all(codex_home).await?;
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_installation_id(path: &Path) -> Result<Option<String>> {
|
||||
let contents = match fs::read_to_string(path).await {
|
||||
Ok(contents) => contents,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
let trimmed = contents.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Uuid::parse_str(trimmed).ok().map(|uuid| uuid.to_string()))
|
||||
}
|
||||
|
||||
async fn write_installation_id(
|
||||
path: PathBuf,
|
||||
installation_id: String,
|
||||
mode: InstallationIdWriteMode,
|
||||
) -> Result<()> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut options = OpenOptions::new();
|
||||
options.write(true);
|
||||
|
||||
match mode {
|
||||
InstallationIdWriteMode::CreateNew => {
|
||||
options.create_new(true);
|
||||
}
|
||||
InstallationIdWriteMode::Overwrite => {
|
||||
options.create(true).truncate(true);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
options.mode(0o600);
|
||||
}
|
||||
|
||||
let mut file = options.open(&path)?;
|
||||
file.write_all(installation_id.as_bytes())?;
|
||||
file.flush()?;
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let metadata = file.metadata()?;
|
||||
let current_mode = metadata.permissions().mode() & 0o777;
|
||||
if current_mode != 0o600 {
|
||||
let mut permissions = metadata.permissions();
|
||||
permissions.set_mode(0o600);
|
||||
file.set_permissions(permissions)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.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, 0o600);
|
||||
}
|
||||
}
|
||||
|
||||
#[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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ mod flags;
|
||||
mod git_info_tests;
|
||||
mod guardian;
|
||||
mod hook_runtime;
|
||||
mod installation_id;
|
||||
pub mod instructions;
|
||||
pub mod landlock;
|
||||
pub mod mcp;
|
||||
@@ -195,6 +196,7 @@ pub(crate) use codex_shell_command::powershell;
|
||||
|
||||
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;
|
||||
|
||||
@@ -7,6 +7,7 @@ use codex_core::ModelProviderInfo;
|
||||
use codex_core::Prompt;
|
||||
use codex_core::ResponseEvent;
|
||||
use codex_core::WireApi;
|
||||
use codex_core::X_CODEX_INSTALLATION_ID_HEADER;
|
||||
use codex_otel::SessionTelemetry;
|
||||
use codex_otel::TelemetryAuthMode;
|
||||
use codex_protocol::ThreadId;
|
||||
@@ -23,6 +24,8 @@ use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
use wiremock::matchers::header;
|
||||
|
||||
const TEST_INSTALLATION_ID: &str = "11111111-1111-4111-8111-111111111111";
|
||||
|
||||
#[tokio::test]
|
||||
async fn responses_stream_includes_subagent_header_on_review() {
|
||||
core_test_support::skip_if_no_network!();
|
||||
@@ -89,6 +92,7 @@ async fn responses_stream_includes_subagent_header_on_review() {
|
||||
let client = ModelClient::new(
|
||||
None,
|
||||
conversation_id,
|
||||
TEST_INSTALLATION_ID.to_string(),
|
||||
provider.clone(),
|
||||
session_source,
|
||||
config.model_verbosity,
|
||||
@@ -132,6 +136,10 @@ async fn responses_stream_includes_subagent_header_on_review() {
|
||||
request.header("x-openai-subagent").as_deref(),
|
||||
Some("review")
|
||||
);
|
||||
assert_eq!(
|
||||
request.header(X_CODEX_INSTALLATION_ID_HEADER).as_deref(),
|
||||
Some(TEST_INSTALLATION_ID)
|
||||
);
|
||||
assert_eq!(request.header("x-codex-sandbox"), None);
|
||||
}
|
||||
|
||||
@@ -202,6 +210,7 @@ async fn responses_stream_includes_subagent_header_on_other() {
|
||||
let client = ModelClient::new(
|
||||
None,
|
||||
conversation_id,
|
||||
TEST_INSTALLATION_ID.to_string(),
|
||||
provider.clone(),
|
||||
session_source,
|
||||
config.model_verbosity,
|
||||
@@ -314,6 +323,7 @@ async fn responses_respects_model_info_overrides_from_config() {
|
||||
let client = ModelClient::new(
|
||||
None,
|
||||
conversation_id,
|
||||
TEST_INSTALLATION_ID.to_string(),
|
||||
provider.clone(),
|
||||
session_source,
|
||||
config.model_verbosity,
|
||||
|
||||
@@ -6,6 +6,7 @@ use codex_core::Prompt;
|
||||
use codex_core::ResponseEvent;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::WireApi;
|
||||
use codex_core::X_CODEX_INSTALLATION_ID_HEADER;
|
||||
use codex_core::auth::AuthCredentialsStoreMode;
|
||||
use codex_core::built_in_model_providers;
|
||||
use codex_core::default_client::originator;
|
||||
@@ -76,6 +77,8 @@ use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
use wiremock::matchers::query_param;
|
||||
|
||||
const INSTALLATION_ID_FILENAME: &str = "installation_id";
|
||||
|
||||
#[expect(clippy::unwrap_used)]
|
||||
fn assert_message_role(request_body: &serde_json::Value, role: &str) {
|
||||
assert_eq!(request_body["role"].as_str().unwrap(), role);
|
||||
@@ -653,10 +656,17 @@ async fn includes_conversation_id_and_model_headers_in_request() {
|
||||
.header("authorization")
|
||||
.expect("authorization header");
|
||||
let request_originator = request.header("originator").expect("originator header");
|
||||
let installation_id =
|
||||
std::fs::read_to_string(test.codex_home_path().join(INSTALLATION_ID_FILENAME))
|
||||
.expect("read installation id");
|
||||
let request_installation_id = request
|
||||
.header(X_CODEX_INSTALLATION_ID_HEADER)
|
||||
.expect("installation id header");
|
||||
|
||||
assert_eq!(request_session_id, session_id.to_string());
|
||||
assert_eq!(request_originator, originator().value);
|
||||
assert_eq!(request_authorization, "Bearer Test API Key");
|
||||
assert_eq!(request_installation_id, installation_id);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -758,11 +768,18 @@ async fn chatgpt_auth_sends_correct_request() {
|
||||
let request_body = request.body_json();
|
||||
|
||||
let session_id = request.header("session_id").expect("session_id header");
|
||||
let installation_id =
|
||||
std::fs::read_to_string(test.codex_home_path().join(INSTALLATION_ID_FILENAME))
|
||||
.expect("read installation id");
|
||||
let request_installation_id = request
|
||||
.header(X_CODEX_INSTALLATION_ID_HEADER)
|
||||
.expect("installation id header");
|
||||
assert_eq!(session_id, thread_id.to_string());
|
||||
|
||||
assert_eq!(request_originator, originator().value);
|
||||
assert_eq!(request_authorization, "Bearer Access Token");
|
||||
assert_eq!(request_chatgpt_account_id, "account_id");
|
||||
assert_eq!(request_installation_id, installation_id);
|
||||
assert!(request_body["stream"].as_bool().unwrap());
|
||||
assert_eq!(
|
||||
request_body["include"][0].as_str().unwrap(),
|
||||
@@ -1837,6 +1854,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
|
||||
let client = ModelClient::new(
|
||||
None,
|
||||
conversation_id,
|
||||
"11111111-1111-4111-8111-111111111111".to_string(),
|
||||
provider.clone(),
|
||||
SessionSource::Exec,
|
||||
config.model_verbosity,
|
||||
|
||||
@@ -8,6 +8,7 @@ use codex_core::ModelProviderInfo;
|
||||
use codex_core::Prompt;
|
||||
use codex_core::ResponseEvent;
|
||||
use codex_core::WireApi;
|
||||
use codex_core::X_CODEX_INSTALLATION_ID_HEADER;
|
||||
use codex_core::X_RESPONSESAPI_INCLUDE_TIMING_METRICS_HEADER;
|
||||
use codex_features::Feature;
|
||||
use codex_otel::SessionTelemetry;
|
||||
@@ -55,6 +56,7 @@ const MODEL: &str = "gpt-5.2-codex";
|
||||
const OPENAI_BETA_HEADER: &str = "OpenAI-Beta";
|
||||
const WS_V2_BETA_HEADER_VALUE: &str = "responses_websockets=2026-02-06";
|
||||
const X_CLIENT_REQUEST_ID_HEADER: &str = "x-client-request-id";
|
||||
const TEST_INSTALLATION_ID: &str = "11111111-1111-4111-8111-111111111111";
|
||||
|
||||
fn assert_request_trace_matches(body: &serde_json::Value, expected_trace: &W3cTraceContext) {
|
||||
let client_metadata = body["client_metadata"]
|
||||
@@ -125,6 +127,10 @@ async fn responses_websocket_streams_request() {
|
||||
handshake.header(X_CLIENT_REQUEST_ID_HEADER),
|
||||
Some(harness.conversation_id.to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
handshake.header(X_CODEX_INSTALLATION_ID_HEADER),
|
||||
Some(TEST_INSTALLATION_ID.to_string())
|
||||
);
|
||||
|
||||
server.shutdown().await;
|
||||
}
|
||||
@@ -1753,6 +1759,7 @@ async fn websocket_harness_with_provider_options(
|
||||
let client = ModelClient::new(
|
||||
None,
|
||||
conversation_id,
|
||||
TEST_INSTALLATION_ID.to_string(),
|
||||
provider.clone(),
|
||||
SessionSource::Exec,
|
||||
config.model_verbosity,
|
||||
|
||||
Reference in New Issue
Block a user