From fa6e9258045bd3a2319f7f227f656e2d05aed236 Mon Sep 17 00:00:00 2001 From: Jeremy Rose Date: Mon, 18 Aug 2025 15:29:44 -0700 Subject: [PATCH 1/3] detect terminal and include in request headers --- codex-rs/core/src/lib.rs | 1 + codex-rs/core/src/terminal.rs | 65 +++++++++++++++++++++++++++++++++ codex-rs/core/src/user_agent.rs | 3 +- 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 codex-rs/core/src/terminal.rs diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 28d35f5376..ad3947aa94 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -49,6 +49,7 @@ pub(crate) mod safety; pub mod seatbelt; pub mod shell; pub mod spawn; +pub mod terminal; pub mod turn_diff_tracker; pub mod user_agent; mod user_notification; diff --git a/codex-rs/core/src/terminal.rs b/codex-rs/core/src/terminal.rs new file mode 100644 index 0000000000..02e0dd1b3a --- /dev/null +++ b/codex-rs/core/src/terminal.rs @@ -0,0 +1,65 @@ +use std::sync::OnceLock; + +static TERMINAL: OnceLock = OnceLock::new(); + +pub fn user_agent() -> String { + TERMINAL.get_or_init(detect_terminal).to_string() +} + +fn detect_terminal() -> String { + if let Ok(tp) = std::env::var("TERM_PROGRAM") { + if !tp.trim().is_empty() { + let ver = std::env::var("TERM_PROGRAM_VERSION").ok(); + return match ver { + Some(v) if !v.trim().is_empty() => format!("{tp}/{v}"), + _ => tp, + }; + } + } + + if let Ok(v) = std::env::var("WEZTERM_VERSION") { + if !v.trim().is_empty() { + return format!("WezTerm/{v}"); + } + return "WezTerm".to_string(); + } + + if std::env::var("KITTY_WINDOW_ID").is_ok() + || std::env::var("TERM") + .map(|t| t.contains("kitty")) + .unwrap_or(false) + { + return "kitty".to_string(); + } + + if std::env::var("ALACRITTY_SOCKET").is_ok() + || std::env::var("TERM") + .map(|t| t == "alacritty") + .unwrap_or(false) + { + return "Alacritty".to_string(); + } + + if let Ok(v) = std::env::var("KONSOLE_VERSION") { + if !v.trim().is_empty() { + return format!("Konsole/{v}"); + } + return "Konsole".to_string(); + } + + if std::env::var("GNOME_TERMINAL_SCREEN").is_ok() { + return "gnome-terminal".to_string(); + } + if let Ok(v) = std::env::var("VTE_VERSION") { + if !v.trim().is_empty() { + return format!("VTE/{v}"); + } + return "VTE".to_string(); + } + + if std::env::var("WT_SESSION").is_ok() { + return "WindowsTerminal".to_string(); + } + + std::env::var("TERM").unwrap_or_else(|_| "unknown".to_string()) +} diff --git a/codex-rs/core/src/user_agent.rs b/codex-rs/core/src/user_agent.rs index ddcfd4b7f1..e91ba368a6 100644 --- a/codex-rs/core/src/user_agent.rs +++ b/codex-rs/core/src/user_agent.rs @@ -4,11 +4,12 @@ pub fn get_codex_user_agent(originator: Option<&str>) -> String { let build_version = env!("CARGO_PKG_VERSION"); let os_info = os_info::get(); format!( - "{}/{build_version} ({} {}; {})", + "{}/{build_version} ({} {}; {}) {}", originator.unwrap_or(DEFAULT_ORIGINATOR), os_info.os_type(), os_info.version(), os_info.architecture().unwrap_or("unknown"), + crate::terminal::user_agent() ) } From 57d34b910b82c09161b64475b92096bce9d0b615 Mon Sep 17 00:00:00 2001 From: Jeremy Rose Date: Wed, 20 Aug 2025 09:44:18 -0700 Subject: [PATCH 2/3] sanitize header value --- codex-rs/core/src/terminal.rs | 111 ++++++++++++++++++---------------- 1 file changed, 59 insertions(+), 52 deletions(-) diff --git a/codex-rs/core/src/terminal.rs b/codex-rs/core/src/terminal.rs index 02e0dd1b3a..02104f8be5 100644 --- a/codex-rs/core/src/terminal.rs +++ b/codex-rs/core/src/terminal.rs @@ -6,60 +6,67 @@ pub fn user_agent() -> String { TERMINAL.get_or_init(detect_terminal).to_string() } +/// Sanitize a header value to be used in a User-Agent string. +/// +/// This function replaces any characters that are not allowed in a User-Agent string with an underscore. +/// +/// # Arguments +/// +/// * `value` - The value to sanitize. +fn is_valid_header_value_char(c: char) -> bool { + c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' || c == '/' +} + +fn sanitize_header_value(value: String) -> String { + value.replace(|c| !is_valid_header_value_char(c), "_") +} + fn detect_terminal() -> String { - if let Ok(tp) = std::env::var("TERM_PROGRAM") { - if !tp.trim().is_empty() { + sanitize_header_value( + if let Ok(tp) = std::env::var("TERM_PROGRAM") + && !tp.trim().is_empty() + { let ver = std::env::var("TERM_PROGRAM_VERSION").ok(); - return match ver { + match ver { Some(v) if !v.trim().is_empty() => format!("{tp}/{v}"), _ => tp, - }; - } - } - - if let Ok(v) = std::env::var("WEZTERM_VERSION") { - if !v.trim().is_empty() { - return format!("WezTerm/{v}"); - } - return "WezTerm".to_string(); - } - - if std::env::var("KITTY_WINDOW_ID").is_ok() - || std::env::var("TERM") - .map(|t| t.contains("kitty")) - .unwrap_or(false) - { - return "kitty".to_string(); - } - - if std::env::var("ALACRITTY_SOCKET").is_ok() - || std::env::var("TERM") - .map(|t| t == "alacritty") - .unwrap_or(false) - { - return "Alacritty".to_string(); - } - - if let Ok(v) = std::env::var("KONSOLE_VERSION") { - if !v.trim().is_empty() { - return format!("Konsole/{v}"); - } - return "Konsole".to_string(); - } - - if std::env::var("GNOME_TERMINAL_SCREEN").is_ok() { - return "gnome-terminal".to_string(); - } - if let Ok(v) = std::env::var("VTE_VERSION") { - if !v.trim().is_empty() { - return format!("VTE/{v}"); - } - return "VTE".to_string(); - } - - if std::env::var("WT_SESSION").is_ok() { - return "WindowsTerminal".to_string(); - } - - std::env::var("TERM").unwrap_or_else(|_| "unknown".to_string()) + } + } else if let Ok(v) = std::env::var("WEZTERM_VERSION") { + if !v.trim().is_empty() { + format!("WezTerm/{v}") + } else { + "WezTerm".to_string() + } + } else if std::env::var("KITTY_WINDOW_ID").is_ok() + || std::env::var("TERM") + .map(|t| t.contains("kitty")) + .unwrap_or(false) + { + "kitty".to_string() + } else if std::env::var("ALACRITTY_SOCKET").is_ok() + || std::env::var("TERM") + .map(|t| t == "alacritty") + .unwrap_or(false) + { + "Alacritty".to_string() + } else if let Ok(v) = std::env::var("KONSOLE_VERSION") { + if !v.trim().is_empty() { + format!("Konsole/{v}") + } else { + "Konsole".to_string() + } + } else if std::env::var("GNOME_TERMINAL_SCREEN").is_ok() { + return "gnome-terminal".to_string(); + } else if let Ok(v) = std::env::var("VTE_VERSION") { + if !v.trim().is_empty() { + format!("VTE/{v}") + } else { + "VTE".to_string() + } + } else if std::env::var("WT_SESSION").is_ok() { + return "WindowsTerminal".to_string(); + } else { + std::env::var("TERM").unwrap_or_else(|_| "unknown".to_string()) + }, + ) } From e876ea394f55a387a1808be8322984a344a1caa2 Mon Sep 17 00:00:00 2001 From: Jeremy Rose Date: Wed, 20 Aug 2025 09:46:51 -0700 Subject: [PATCH 3/3] fix test --- codex-rs/core/src/user_agent.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/codex-rs/core/src/user_agent.rs b/codex-rs/core/src/user_agent.rs index e91ba368a6..a63170cebd 100644 --- a/codex-rs/core/src/user_agent.rs +++ b/codex-rs/core/src/user_agent.rs @@ -28,9 +28,10 @@ mod tests { fn test_macos() { use regex_lite::Regex; let user_agent = get_codex_user_agent(None); - let re = - Regex::new(r"^codex_cli_rs/\d+\.\d+\.\d+ \(Mac OS \d+\.\d+\.\d+; (x86_64|arm64)\)$") - .unwrap(); + let re = Regex::new( + r"^codex_cli_rs/\d+\.\d+\.\d+ \(Mac OS \d+\.\d+\.\d+; (x86_64|arm64)\) (\S+)$", + ) + .unwrap(); assert!(re.is_match(&user_agent)); } }