Files
codex/prs/bolinfest/PR-2437.md
2025-09-02 15:17:45 -07:00

328 lines
9.7 KiB
Markdown

# PR #2437: detect terminal and include in request headers
- URL: https://github.com/openai/codex/pull/2437
- Author: nornagon-openai
- Created: 2025-08-18 22:37:50 UTC
- Updated: 2025-08-20 16:54:35 UTC
- Changes: +79/-4, Files changed: 3, Commits: 3
## Description
This adds the terminal version to the UA header.
## Full Diff
```diff
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..02104f8be5
--- /dev/null
+++ b/codex-rs/core/src/terminal.rs
@@ -0,0 +1,72 @@
+use std::sync::OnceLock;
+
+static TERMINAL: OnceLock<String> = OnceLock::new();
+
+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 {
+ 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();
+ match ver {
+ Some(v) if !v.trim().is_empty() => format!("{tp}/{v}"),
+ _ => tp,
+ }
+ } 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())
+ },
+ )
+}
diff --git a/codex-rs/core/src/user_agent.rs b/codex-rs/core/src/user_agent.rs
index ddcfd4b7f1..a63170cebd 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()
)
}
@@ -27,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));
}
}
```
## Review Comments
### codex-rs/core/src/terminal.rs
- Created: 2025-08-18 22:40:45 UTC | Link: https://github.com/openai/codex/pull/2437#discussion_r2283644516
```diff
@@ -0,0 +1,65 @@
+use std::sync::OnceLock;
+
+static TERMINAL: OnceLock<String> = 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();
```
> Consider:
>
> ```suggestion
> return if !v.trim().is_empty() {
> format!("WezTerm/{v}")
> } else {
> "WezTerm".to_string()
> };
> ```
- Created: 2025-08-18 22:42:18 UTC | Link: https://github.com/openai/codex/pull/2437#discussion_r2283646175
```diff
@@ -0,0 +1,65 @@
+use std::sync::OnceLock;
+
+static TERMINAL: OnceLock<String> = 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() {
```
> same trick as above, but either way
- Created: 2025-08-18 22:42:35 UTC | Link: https://github.com/openai/codex/pull/2437#discussion_r2283646455
```diff
@@ -0,0 +1,65 @@
+use std::sync::OnceLock;
+
+static TERMINAL: OnceLock<String> = 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() {
```
> samezies
- Created: 2025-08-18 22:45:58 UTC | Link: https://github.com/openai/codex/pull/2437#discussion_r2283652785
```diff
@@ -0,0 +1,65 @@
+use std::sync::OnceLock;
+
+static TERMINAL: OnceLock<String> = OnceLock::new();
+
+pub fn user_agent() -> String {
+ TERMINAL.get_or_init(detect_terminal).to_string()
+}
+
+fn detect_terminal() -> String {
```
> I don't know if there is any pathological edge case here, but should we verify that the result of `detect_terminal()` is safe to include as an HTTP header? I don't know why `TERM_PROGRAM_VERSION` would contain newlines or other funky characters, but better safe than sorry?
### codex-rs/core/src/user_agent.rs
- Created: 2025-08-18 22:46:27 UTC | Link: https://github.com/openai/codex/pull/2437#discussion_r2283653832
```diff
@@ -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} ({} {}; {}) {}",
```
> Though perhaps the reqwest library or whatever consumes this checks to ensure header names and values are safe?